Skip to content

Commit a41d769

Browse files
committed
Limited slow check to downloading items
1 parent 6128f43 commit a41d769

File tree

3 files changed

+122
-69
lines changed

3 files changed

+122
-69
lines changed

src/jobs/remove_metadata_missing.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ class RemoveMetadataMissing(RemovalJob):
66
blocklist = True
77

88
async def _find_affected_items(self):
9-
conditions = [("queued", "qBittorrent is downloading metadata")]
9+
# conditions = [("queued", "qBittorrent is downloading metadata")]
10+
conditions = ["paused"]
1011
return self.queue_manager.filter_queue(self.queue, conditions)

src/jobs/remove_slow.py

Lines changed: 45 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -16,62 +16,79 @@ async def _find_affected_items(self):
1616
await self.update_bandwidth_usage()
1717

1818
for item in self.queue:
19-
if not self._check_required_keys(item):
19+
20+
# Already checked downloadId -> skip
21+
if self._checked_before(item, checked_ids):
2022
continue
2123

22-
download_id = item["downloadId"]
24+
# Keys not present -> skip
25+
if self._missing_keys(item):
26+
continue
2327

24-
if download_id in checked_ids:
25-
continue # One downloadId may occur in multiple items - only check once for all of them per iteration
26-
checked_ids.add(download_id)
28+
# Not Downloading -> skip
29+
if self._not_downloading(item):
30+
continue
2731

32+
# Is Usenet -> skip
2833
if self._is_usenet(item):
2934
continue # No need to check for speed for usenet, since there users pay for speed
3035

36+
# Completed but stuck -> skip
3137
if self._is_completed_but_stuck(item):
3238
logger.info(
3339
f">>> '{self.job_name}' detected download marked as slow as well as completed. Files most likely in process of being moved. Not removing: {item['title']}",
3440
)
3541
continue
3642

37-
if self._high_bandwidth_usage(download_client=item["download_client"], download_client_type=item["download_client_type"]):
43+
# High bandwidth usage -> skip
44+
if self._high_bandwidth_usage(item):
3845
continue
3946

4047
downloaded, previous, increment, speed = await self._get_progress_stats(
4148
item
4249
)
43-
if self._is_slow(speed):
44-
affected_items.append(item)
45-
logger.debug(
46-
f'remove_slow/slow speed detected: {item["title"]} '
47-
f"(Speed: {speed} KB/s, KB now: {downloaded}, KB previous: {previous}, "
48-
f"Diff: {increment}, In Minutes: {self.settings.general.timer})",
49-
)
50+
51+
# Not slow -> skip
52+
if self._not_slow(speed):
53+
continue
54+
55+
# None of above, hence truly slow
56+
affected_items.append(item)
57+
logger.debug(
58+
f'remove_slow/slow speed detected: {item["title"]} '
59+
f"(Speed: {speed} KB/s, KB now: {downloaded}, KB previous: {previous}, "
60+
f"Diff: {increment}, In Minutes: {self.settings.general.timer})",
61+
)
5062

5163
return affected_items
5264

5365
@staticmethod
54-
def _check_required_keys(item) -> bool:
66+
def _checked_before(item, checked_ids):
67+
download_id = item.get("downloadId", "None")
68+
if download_id in checked_ids:
69+
return True # One downloadId may occur in multiple items - only check once for all of them per iteration
70+
checked_ids.add(download_id)
71+
return False
72+
73+
@staticmethod
74+
def _missing_keys(item) -> bool:
5575
required_keys = {"downloadId", "size", "sizeleft", "status", "protocol", "download_client", "download_client_type"}
56-
return required_keys.issubset(item)
76+
return not required_keys.issubset(item)
5777

5878
@staticmethod
5979
def _is_usenet(item) -> bool:
6080
return item.get("protocol") == "usenet"
6181

82+
@staticmethod
83+
def _not_downloading(item) -> bool:
84+
return item.get("status") != "downloading"
85+
6286
@staticmethod
6387
def _is_completed_but_stuck(item) -> bool:
64-
return (
65-
item["status"] == "downloading"
66-
and item["size"] > 0
67-
and item["sizeleft"] == 0
68-
)
88+
return item["size"] > 0 and item["sizeleft"] == 0
6989

70-
def _is_slow(self, speed):
71-
return (
72-
speed is not None
73-
and speed < self.job.min_speed
74-
)
90+
def _not_slow(self, speed):
91+
return speed is None or speed >= self.job.min_speed
7592

7693
async def _get_progress_stats(self, item):
7794
download_id = item["downloadId"]
@@ -108,7 +125,9 @@ def _compute_increment_and_speed(self, download_id, current_progress):
108125
return previous_progress, increment, speed
109126

110127
@staticmethod
111-
def _high_bandwidth_usage(download_client, download_client_type):
128+
def _high_bandwidth_usage(item):
129+
download_client=item["download_client"]
130+
download_client_type=item["download_client_type"]
112131
if download_client_type == "qbittorrent":
113132
if download_client.bandwidth_usage > DISABLE_OVER_BANDWIDTH_USAGE:
114133
return True

tests/jobs/test_remove_slow.py

Lines changed: 75 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,24 @@
66

77

88
# pylint: disable=W0212
9+
@pytest.mark.parametrize(
10+
"checked_ids, item, expected_return, expected_checked_ids",
11+
[
12+
(set(), {"downloadId": "id1"}, False, {"id1"}),
13+
({"id1"}, {"downloadId": "id1"}, True, {"id1"}),
14+
(set(), {"downloadId": "id2"}, False, {"id2"}),
15+
(set(), {}, False, {"None"}), # no downloadId key, treated as "None"
16+
({"None"}, {}, True, {"None"}),
17+
({"id1", "id2"}, {"downloadId": "id3"}, False, {"id1", "id2", "id3"}),
18+
({"id1", "id2"}, {"downloadId": "id1"}, True, {"id1", "id2"}),
19+
],
20+
)
21+
def test_checked_before(checked_ids, item, expected_return, expected_checked_ids):
22+
result = RemoveSlow._checked_before(item, checked_ids)
23+
assert result == expected_return
24+
assert checked_ids == expected_checked_ids
25+
26+
927
@pytest.mark.asyncio
1028
@pytest.mark.parametrize(
1129
("item", "expected_result"),
@@ -21,7 +39,7 @@
2139
"download_client": AsyncMock(),
2240
"download_client_type": "qBittorrent",
2341
},
24-
True,
42+
False,
2543
),
2644
(
2745
# Invalid: missing one
@@ -33,21 +51,35 @@
3351
"protocol": "torrent",
3452
"download_client": AsyncMock(),
3553
},
36-
False,
54+
True,
3755
),
3856
(
3957
# Invalid: missing multiple
4058
{
4159
"size": 1000,
4260
"sizeleft": 500,
4361
},
44-
False,
62+
True,
4563
),
4664
],
4765
)
48-
async def test_check_required_keys(item, expected_result):
66+
async def test_missing_keys(item, expected_result):
4967
removal_job = shared_fix_affected_items(RemoveSlow)
50-
result = removal_job._check_required_keys(item)
68+
result = removal_job._missing_keys(item)
69+
assert result == expected_result
70+
71+
72+
@pytest.mark.parametrize(
73+
("item", "expected_result"),
74+
[
75+
({"status": "downloading"}, False),
76+
({"status": "completed"}, True),
77+
({"status": "paused"}, True),
78+
({}, True), # no status key
79+
],
80+
)
81+
def test_not_downloading(item, expected_result):
82+
result = RemoveSlow._not_downloading(item)
5183
assert result == expected_result
5284

5385

@@ -68,9 +100,9 @@ def test_is_usenet(item, expected_result):
68100
@pytest.mark.parametrize(
69101
("item", "expected_result"),
70102
[
71-
({"status": "downloading", "size": 1000, "sizeleft": 0}, True),
72-
({"status": "completed", "size": 1000, "sizeleft": 0}, False),
73-
({"status": "downloading", "size": 0, "sizeleft": 0}, False),
103+
({"size": 1000, "sizeleft": 0}, True),
104+
({"size": 1000, "sizeleft": 1}, False),
105+
({"size": 0, "sizeleft": 0}, False),
74106
],
75107
)
76108
def test_is_completed_but_stuck(item, expected_result):
@@ -82,17 +114,17 @@ def test_is_completed_but_stuck(item, expected_result):
82114
@pytest.mark.parametrize(
83115
("speed", "expected_result"),
84116
[
85-
(None, False), # speed is None -> not slow
86-
(0, True), # speed less than min_speed -> slow (assuming min_speed > 0)
87-
(5, True), # speed less than min_speed
88-
(10, False), # speed equal or above min_speed (assuming min_speed=10)
89-
(15, False), # speed above min_speed
117+
(None, True), # speed is None -> not slow
118+
(0, False), # speed less than min_speed -> slow (assuming min_speed > 0)
119+
(5, False), # speed less than min_speed
120+
(10, True), # speed equal or above min_speed (assuming min_speed=10)
121+
(15, True), # speed above min_speed
90122
],
91123
)
92-
def test_is_slow(speed, expected_result):
124+
def test_not_slow(speed, expected_result):
93125
removal_job = shared_fix_affected_items(RemoveSlow)
94126
removal_job.job.min_speed = 10
95-
result = removal_job._is_slow(speed)
127+
result = removal_job._not_slow(speed)
96128
assert result == expected_result
97129

98130

@@ -228,8 +260,11 @@ class DummyClient:
228260
def __init__(self, usage):
229261
self.bandwidth_usage = usage
230262

231-
download_client = DummyClient(bandwidth_usage)
232-
result = RemoveSlow._high_bandwidth_usage(download_client, download_client_type)
263+
item = {
264+
"download_client": DummyClient(bandwidth_usage),
265+
"download_client_type": download_client_type,
266+
}
267+
result = RemoveSlow._high_bandwidth_usage(item)
233268
assert result == expected
234269

235270

@@ -299,51 +334,49 @@ async def test_update_bandwidth_usage_calls_once_per_client():
299334
@pytest.mark.parametrize(
300335
"queue_item, should_be_affected",
301336
[
337+
# Already checked downloadId -> skip (simulate by repeating downloadId)
338+
({"downloadId": "checked_before"}, False),
339+
302340
# Keys not present -> skip
303-
({"downloadClient": "client1"}, False),
341+
({"downloadId": "keys_missing"}, False),
304342
305-
# Already checked downloadId -> skip (simulate by repeating downloadId)
306-
({"downloadId": "checked_before", "download_client": MagicMock(), "download_client_type": "qbittorrent"}, False),
343+
# Not Downloading -> skip
344+
({"downloadId": "not_downloading"}, False),
307345
308346
# Is Usenet -> skip
309-
({"downloadId": "usenet", "download_client": MagicMock(), "download_client_type": "qbittorrent"}, False),
347+
({"downloadId": "usenet"}, False),
310348
311349
# Completed but stuck -> skip
312-
({"downloadId": "stuck", "download_client": MagicMock(), "download_client_type": "qbittorrent"}, False),
350+
({"downloadId": "completed_but_stuck"}, False),
313351
314352
# High bandwidth usage -> skip
315-
({"downloadId": "highbw", "download_client": MagicMock(), "download_client_type": "qbittorrent"}, False),
353+
({"downloadId": "high_bandwidth"}, False),
316354
317355
# Not slow -> skip
318-
({"downloadId": "notslow", "download_client": MagicMock(), "download_client_type": "qbittorrent"}, False),
356+
({"downloadId": "not_slow"}, False),
319357
320-
# None of above, should be affected
321-
({"downloadId": "good", "title": "Good Item", "download_client": MagicMock(), "download_client_type": "qbittorrent"}, True),
358+
# None of above, hence truly slow
359+
({"downloadId": "good"}, True),
322360
],
323361
)
324362
async def test_find_affected_items_simple(queue_item, should_be_affected):
325363
# Add minimum fields required
326364
queue_item["title"] = queue_item.get("downloadId", "dummy")
327365
removal_job = shared_fix_affected_items(RemoveSlow, queue_data=[queue_item])
328366

329-
# Setup queue differently based on test case
330-
if queue_item.get("downloadId") == "dup":
331-
# Add duplicate entries to test skipping by checked_ids
332-
removal_job.queue = [queue_item, queue_item]
333-
else:
334-
removal_job.queue = [queue_item]
335-
336-
# Mock methods
337-
removal_job._check_required_keys = MagicMock(return_value="downloadId" in queue_item)
338-
removal_job._is_usenet = MagicMock(return_value=queue_item.get("downloadId") == "usenet")
339-
removal_job._is_completed_but_stuck = MagicMock(return_value=queue_item.get("downloadId") == "stuck")
340-
removal_job._high_bandwidth_usage = MagicMock(return_value=queue_item.get("downloadId") == "highbw")
341-
removal_job._get_progress_stats = AsyncMock(return_value=(1000, 900, 100, 10)) # arbitrary numbers
342-
removal_job._is_slow = MagicMock(return_value=queue_item.get("downloadId") == "good")
343-
344-
# Mock add_download_client_to_queue_items and update_bandwidth_usage as no-ops
367+
# Mock async methods
345368
removal_job.add_download_client_to_queue_items = AsyncMock()
346369
removal_job.update_bandwidth_usage = AsyncMock()
370+
removal_job._get_progress_stats = AsyncMock(return_value=(1000, 900, 100, 10))
371+
372+
# Setup checks to pass except in for the designated tests
373+
removal_job._checked_before = lambda item, checked_ids: item.get("downloadId") == "checked_before"
374+
removal_job._missing_keys = lambda item: item.get("downloadId") == "keys_missing"
375+
removal_job._not_downloading = lambda item: item.get("downloadId") == "not_downloading"
376+
removal_job._is_usenet = lambda item: item.get("downloadId") == "usenet"
377+
removal_job._is_completed_but_stuck = lambda item: item.get("downloadId") == "completed_but_stuck"
378+
removal_job._high_bandwidth_usage = lambda download_client, download_client_type=None: queue_item.get("downloadId") == "high_bandwidth"
379+
removal_job._not_slow = lambda speed: queue_item.get("downloadId") == "not_slow"
347380

348381
# Run the method under test
349382
affected_items = await removal_job._find_affected_items()

0 commit comments

Comments
 (0)