qBitrr2 5.8.7__py3-none-any.whl → 5.8.8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,293 @@
1
+ """qBit Category Manager - Manages torrents in qBit-managed categories with seeding settings."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import logging
6
+ import time
7
+ from typing import TYPE_CHECKING
8
+
9
+ from qBitrr.errors import DelayLoopException
10
+
11
+ if TYPE_CHECKING:
12
+ from qbittorrentapi import TorrentDictionary
13
+
14
+ from qBitrr.main import qBitManager
15
+
16
+ # Sleep timer between processing loops (seconds)
17
+ LOOP_SLEEP_TIMER = 10
18
+
19
+
20
+ class qBitCategoryManager:
21
+ """Manages torrents in qBit-managed categories with custom seeding settings."""
22
+
23
+ def __init__(self, instance_name: str, qbit_manager: qBitManager, config: dict):
24
+ """
25
+ Initialize the qBit category manager.
26
+
27
+ Args:
28
+ instance_name: Name of the qBit instance
29
+ qbit_manager: Reference to qBitManager
30
+ config: Configuration dict containing:
31
+ - managed_categories: List of category names
32
+ - default_seeding: Default seeding settings
33
+ - category_overrides: Per-category seeding overrides
34
+ """
35
+ self.instance_name = instance_name
36
+ self.qbit_manager = qbit_manager
37
+ self.managed_categories = config.get("managed_categories", [])
38
+ self.default_seeding = config.get("default_seeding", {})
39
+ self.category_overrides = config.get("category_overrides", {})
40
+ self.logger = logging.getLogger(f"qBitrr.qBitCategory.{instance_name}")
41
+
42
+ self.logger.info(
43
+ "Initialized qBit category manager for instance '%s' with %d categories: %s",
44
+ instance_name,
45
+ len(self.managed_categories),
46
+ ", ".join(self.managed_categories),
47
+ )
48
+
49
+ def get_client(self):
50
+ """Get the qBit client for this instance."""
51
+ return self.qbit_manager.get_client(self.instance_name)
52
+
53
+ def get_seeding_config(self, category: str) -> dict:
54
+ """
55
+ Get seeding configuration for a category.
56
+
57
+ Args:
58
+ category: Category name
59
+
60
+ Returns:
61
+ Seeding config dict (per-category override or default)
62
+ """
63
+ if category in self.category_overrides:
64
+ self.logger.debug("Using category-specific seeding config for '%s'", category)
65
+ return self.category_overrides[category]
66
+ return self.default_seeding
67
+
68
+ def process_torrents(self):
69
+ """Process all torrents in managed categories."""
70
+ client = self.get_client()
71
+ if not client:
72
+ self.logger.warning(
73
+ "qBit client not available for instance '%s', skipping", self.instance_name
74
+ )
75
+ return
76
+
77
+ for category in self.managed_categories:
78
+ try:
79
+ # Fetch torrents in this category
80
+ torrents = client.torrents_info(category=category)
81
+ self.logger.debug(
82
+ "Processing %d torrents in category '%s'",
83
+ len(torrents),
84
+ category,
85
+ )
86
+
87
+ for torrent in torrents:
88
+ self._process_single_torrent(torrent, category)
89
+
90
+ except Exception as e:
91
+ self.logger.error(
92
+ "Error processing category '%s': %s",
93
+ category,
94
+ e,
95
+ exc_info=True,
96
+ )
97
+
98
+ def _process_single_torrent(self, torrent: TorrentDictionary, category: str):
99
+ """
100
+ Process a single torrent - apply seeding settings and check removal.
101
+
102
+ Args:
103
+ torrent: qBittorrent torrent object
104
+ category: Category name
105
+ """
106
+ try:
107
+ config = self.get_seeding_config(category)
108
+
109
+ # Apply seeding limits
110
+ self._apply_seeding_limits(torrent, config)
111
+
112
+ # Check if torrent should be removed
113
+ if self._should_remove_torrent(torrent, config):
114
+ self._remove_torrent(torrent, category)
115
+
116
+ except Exception as e:
117
+ self.logger.error(
118
+ "Error processing torrent '%s' in category '%s': %s",
119
+ torrent.name,
120
+ category,
121
+ e,
122
+ exc_info=True,
123
+ )
124
+
125
+ def _apply_seeding_limits(self, torrent: TorrentDictionary, config: dict):
126
+ """
127
+ Apply seeding limits to a torrent.
128
+
129
+ Args:
130
+ torrent: qBittorrent torrent object
131
+ config: Seeding configuration dict
132
+ """
133
+ ratio_limit = config.get("MaxUploadRatio", -1)
134
+ time_limit = config.get("MaxSeedingTime", -1)
135
+
136
+ # Prepare share limits
137
+ share_limits = {}
138
+ if ratio_limit > 0:
139
+ share_limits["ratio_limit"] = ratio_limit
140
+ if time_limit > 0:
141
+ share_limits["seeding_time_limit"] = time_limit
142
+
143
+ # Apply share limits if any
144
+ if share_limits:
145
+ try:
146
+ torrent.set_share_limits(**share_limits)
147
+ self.logger.debug(
148
+ "Applied share limits to '%s': %s",
149
+ torrent.name,
150
+ share_limits,
151
+ )
152
+ except Exception as e:
153
+ self.logger.error(
154
+ "Failed to set share limits for '%s': %s",
155
+ torrent.name,
156
+ e,
157
+ )
158
+
159
+ # Apply download rate limit
160
+ dl_limit = config.get("DownloadRateLimitPerTorrent", -1)
161
+ if dl_limit >= 0:
162
+ try:
163
+ # qBittorrent expects rate limits in bytes/s
164
+ # Config is in KB/s, so multiply by 1024
165
+ limit_bytes = dl_limit * 1024 if dl_limit > 0 else -1
166
+ torrent.set_download_limit(limit=limit_bytes)
167
+ self.logger.debug(
168
+ "Set download limit for '%s': %d KB/s",
169
+ torrent.name,
170
+ dl_limit,
171
+ )
172
+ except Exception as e:
173
+ self.logger.error(
174
+ "Failed to set download limit for '%s': %s",
175
+ torrent.name,
176
+ e,
177
+ )
178
+
179
+ # Apply upload rate limit
180
+ ul_limit = config.get("UploadRateLimitPerTorrent", -1)
181
+ if ul_limit >= 0:
182
+ try:
183
+ # qBittorrent expects rate limits in bytes/s
184
+ # Config is in KB/s, so multiply by 1024
185
+ limit_bytes = ul_limit * 1024 if ul_limit > 0 else -1
186
+ torrent.set_upload_limit(limit=limit_bytes)
187
+ self.logger.debug(
188
+ "Set upload limit for '%s': %d KB/s",
189
+ torrent.name,
190
+ ul_limit,
191
+ )
192
+ except Exception as e:
193
+ self.logger.error(
194
+ "Failed to set upload limit for '%s': %s",
195
+ torrent.name,
196
+ e,
197
+ )
198
+
199
+ def _should_remove_torrent(self, torrent: TorrentDictionary, config: dict) -> bool:
200
+ """
201
+ Check if torrent meets removal conditions.
202
+
203
+ Args:
204
+ torrent: qBittorrent torrent object
205
+ config: Seeding configuration dict
206
+
207
+ Returns:
208
+ True if torrent should be removed, False otherwise
209
+ """
210
+ remove_mode = config.get("RemoveTorrent", -1)
211
+
212
+ if remove_mode == -1:
213
+ return False # Never remove
214
+
215
+ ratio_limit = config.get("MaxUploadRatio", -1)
216
+ time_limit = config.get("MaxSeedingTime", -1)
217
+
218
+ # Check if limits are met
219
+ ratio_met = ratio_limit > 0 and torrent.ratio >= ratio_limit
220
+ time_met = time_limit > 0 and torrent.seeding_time >= time_limit
221
+
222
+ # Determine removal based on mode
223
+ if remove_mode == 1: # Remove on ratio only
224
+ return ratio_met
225
+ elif remove_mode == 2: # Remove on time only
226
+ return time_met
227
+ elif remove_mode == 3: # Remove on OR (either condition)
228
+ return ratio_met or time_met
229
+ elif remove_mode == 4: # Remove on AND (both conditions)
230
+ return ratio_met and time_met
231
+
232
+ return False
233
+
234
+ def _remove_torrent(self, torrent: TorrentDictionary, category: str):
235
+ """
236
+ Remove torrent that met seeding goals.
237
+
238
+ Args:
239
+ torrent: qBittorrent torrent object
240
+ category: Category name
241
+ """
242
+ try:
243
+ self.logger.info(
244
+ "Removing torrent '%s' from category '%s' " "(ratio: %.2f, seeding time: %ds)",
245
+ torrent.name,
246
+ category,
247
+ torrent.ratio,
248
+ torrent.seeding_time,
249
+ )
250
+ # Remove from qBit but keep files
251
+ torrent.delete(delete_files=False)
252
+ except Exception as e:
253
+ self.logger.error(
254
+ "Failed to remove torrent '%s': %s",
255
+ torrent.name,
256
+ e,
257
+ exc_info=True,
258
+ )
259
+
260
+ def run_processing_loop(self):
261
+ """
262
+ Main processing loop for qBit-managed categories.
263
+
264
+ This runs in a separate process and continuously processes torrents.
265
+ """
266
+ self.logger.info(
267
+ "Starting processing loop for qBit category manager '%s'",
268
+ self.instance_name,
269
+ )
270
+
271
+ while not self.qbit_manager.shutdown_event.is_set():
272
+ try:
273
+ self.process_torrents()
274
+ time.sleep(LOOP_SLEEP_TIMER)
275
+
276
+ except DelayLoopException as e:
277
+ # Intentional delay requested
278
+ self.logger.debug("Delaying loop for %d seconds", e.length)
279
+ time.sleep(e.length)
280
+
281
+ except Exception as e:
282
+ self.logger.error(
283
+ "Error in qBit category processing loop: %s",
284
+ e,
285
+ exc_info=True,
286
+ )
287
+ # Sleep before retrying to avoid rapid error loops
288
+ time.sleep(60)
289
+
290
+ self.logger.info(
291
+ "Shutdown event received, stopping processing loop for '%s'",
292
+ self.instance_name,
293
+ )