From fc5fedbde223b4625d2133e889e49f8907d1f601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Gruszczy=C5=84ski?= Date: Tue, 5 May 2026 17:29:45 +0200 Subject: [PATCH] fix queue --- pytorrent/services/smart_queue.py | 106 +++++++++++++++++++----------- 1 file changed, 68 insertions(+), 38 deletions(-) diff --git a/pytorrent/services/smart_queue.py b/pytorrent/services/smart_queue.py index 33e3f35..be779b2 100644 --- a/pytorrent/services/smart_queue.py +++ b/pytorrent/services/smart_queue.py @@ -469,19 +469,19 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool = to_pause: list[dict[str, Any]] = pause_rank[:max(0, len(downloading) - max_active)] pause_hashes = {str(t.get('hash') or '') for t in to_pause} - # Note: Rotacja stalled działa tylko przy pełnej kolejce. Gdy brakuje slotów, Smart Queue ma - # najpierw dobrać brakujące pozycje, a nie pauzować już istniejące lub błędnie uznane za stalled. - if candidates and len(downloading) >= max_active: - replaceable_stalled = [t for t in stalled if str(t.get('hash') or '') not in pause_hashes] - for t in replaceable_stalled[:max(0, len(candidates) - len(to_pause))]: - to_pause.append(t) - pause_hashes.add(str(t.get('hash') or '')) + # Note: Stalled jest wymieniany nie tylko przy pelnej kolejce. Najpierw wypelniamy wolne sloty, + # a dopiero nadmiar kandydatow zuzywamy na rotacje martwych/stalled pobran. + free_slots_before_pause = max(0, max_active - max(0, len(downloading) - len(to_pause))) + stalled_rotation_slots = max(0, len(candidates) - free_slots_before_pause) + for t in stalled: + h = str(t.get('hash') or '') + if not h or h in pause_hashes or stalled_rotation_slots <= 0: + continue + to_pause.append(t) + pause_hashes.add(h) + stalled_rotation_slots -= 1 active_after_pause = max(0, len(downloading) - len(to_pause)) - available_slots = max(0, max_active - active_after_pause) - to_resume = candidates[:available_slots] - # Note: Pozycje poza bieżącą pulą startu zostają jawnie oznaczone jako oczekujące Smart Queue. - to_label_waiting = candidates[available_slots:] c = rtorrent.client_for(profile) rtorrent_cap = _ensure_rtorrent_download_cap(c, max_active) @@ -492,19 +492,67 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool = start_no_effect: list[dict[str, Any]] = [] resume_requested: list[str] = [] start_results: list[dict[str, Any]] = [] + attempted_hashes: set[str] = set() for t in to_pause: + h = str(t.get('hash') or '') + if not h: + continue try: - c.call('d.pause', t['hash']) + c.call('d.pause', h) if not _mark_auto_paused(c, profile_id, t): - label_failed.append(t['hash']) - paused.append(t['hash']) + label_failed.append(h) + paused.append(h) except Exception: pass - for t in to_label_waiting: + candidate_queue = [t for t in candidates if str(t.get('hash') or '') and str(t.get('hash') or '') not in pause_hashes] + active_slots = active_after_pause + + # Note: Resume dziala teraz w petli do pelnego limitu z ustawien. Gdy batch nie przejdzie + # na d.is_active=1, Smart Queue nie zatrzymuje sie, tylko probuje nastepnych kandydatow. + while candidate_queue and active_slots < max_active: + slots_left = max_active - active_slots + batch = candidate_queue[:slots_left] + candidate_queue = candidate_queue[slots_left:] + batch_requested: list[str] = [] + + for t in batch: + h = str(t.get('hash') or '') + if not h: + continue + attempted_hashes.add(h) + try: + result = _start_download(c, t) + start_results.append(result) + resume_requested.append(h) + batch_requested.append(h) + except Exception as exc: + start_failed.append({'hash': h, 'error': str(exc)}) + + active_verified, batch_no_effect = _verify_started_downloads(c, batch_requested) + start_no_effect.extend(batch_no_effect) + for h in active_verified: + if h not in resumed: + _restore_auto_label(c, profile_id, h, None) + resumed.append(h) + active_slots += len([h for h in active_verified if h]) + + if not batch_requested: + break + + resumed_set = set(resumed) + waiting_hashes = { + str(t.get('hash') or '') + for t in candidates + if str(t.get('hash') or '') and str(t.get('hash') or '') not in pause_hashes and str(t.get('hash') or '') not in resumed_set + } + + # Note: Kazdy kandydat niewznowiony w tej rundzie zostaje oznaczony jako oczekujacy, + # dzieki czemu kolejne cykle nadal dobieraja go z pauzy/labela Smart Queue. + for t in candidates: h = str(t.get('hash') or '') - if not h or h in pause_hashes: + if not h or h not in waiting_hashes: continue try: if not _mark_auto_paused(c, profile_id, t): @@ -512,30 +560,12 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool = except Exception: label_failed.append(h) - # Note: Startujemy całą pulę kandydatów w jednej rundzie. Label zdejmujemy po zaakceptowanym RPC, - # bo rTorrent może trzymać część pozycji w swojej kolejce z active=0 mimo poprawnego d.start/d.resume. - for t in to_resume: - h = str(t.get('hash') or '') - if not h: - continue - try: - result = _start_download(c, t) - start_results.append(result) - resume_requested.append(h) - except Exception as exc: - start_failed.append({'hash': h, 'error': str(exc)}) - - active_verified, start_no_effect = _verify_started_downloads(c, resume_requested) - for h in active_verified: - _restore_auto_label(c, profile_id, h, None) - # Note: Historia pokazuje tylko torrenty faktycznie zdjęte z pauzy, a nie samą liczbę wysłanych komend. - resumed = list(active_verified) keep_labels = ( set(paused) - | {str(t.get('hash') or '') for t in to_label_waiting} - | {str(t.get('hash') or '') for t in stopped if str(t.get('label') or '') == SMART_QUEUE_LABEL and str(t.get('hash') or '') not in set(resumed)} + | waiting_hashes + | {str(t.get('hash') or '') for t in stopped if str(t.get('label') or '') == SMART_QUEUE_LABEL and str(t.get('hash') or '') not in resumed_set} ) restored = _cleanup_auto_labels(c, profile_id, torrents, keep_labels, manage_stopped) - details = {'excluded': len(excluded), 'enabled': bool(settings.get('enabled')), 'auto_label': SMART_QUEUE_LABEL, 'labels_restored': restored, 'labels_failed': label_failed, 'start_failed': start_failed, 'start_no_effect': start_no_effect, 'start_results': start_results, 'resume_requested': resume_requested, 'active_verified': active_verified, 'waiting_labeled': len(to_label_waiting), 'manage_stopped': manage_stopped, 'max_active_downloads': max_active, 'active_before': len(downloading), 'active_after_expected': active_after_pause + len(resumed), 'paused_planned': len(to_pause), 'resumed_planned': len(to_resume), 'rtorrent_cap': rtorrent_cap} + details = {'excluded': len(excluded), 'enabled': bool(settings.get('enabled')), 'auto_label': SMART_QUEUE_LABEL, 'labels_restored': restored, 'labels_failed': label_failed, 'start_failed': start_failed, 'start_no_effect': start_no_effect, 'start_results': start_results, 'resume_requested': resume_requested, 'active_verified': resumed, 'attempted_count': len(attempted_hashes), 'waiting_labeled': len(waiting_hashes), 'manage_stopped': manage_stopped, 'max_active_downloads': max_active, 'active_before': len(downloading), 'active_after_expected': active_slots, 'paused_planned': len(to_pause), 'resumed_planned': len(attempted_hashes), 'stalled_detected': len(stalled), 'stalled_paused': len([h for h in paused if h in stalled_hashes]), 'rtorrent_cap': rtorrent_cap} add_history(profile_id, 'force_check' if force else 'auto_check', paused, resumed, len(torrents), details, user_id) - return {'ok': True, 'enabled': bool(settings.get('enabled')), 'paused': paused, 'resumed': resumed, 'resume_requested': resume_requested, 'waiting_labeled': len(to_label_waiting), 'labels_restored': restored, 'labels_failed': label_failed, 'start_failed': start_failed, 'start_no_effect': start_no_effect, 'active_verified': active_verified, 'rtorrent_cap': rtorrent_cap, 'checked': len(torrents), 'excluded': len(excluded), 'settings': settings} + return {'ok': True, 'enabled': bool(settings.get('enabled')), 'paused': paused, 'resumed': resumed, 'resume_requested': resume_requested, 'waiting_labeled': len(waiting_hashes), 'labels_restored': restored, 'labels_failed': label_failed, 'start_failed': start_failed, 'start_no_effect': start_no_effect, 'active_verified': resumed, 'rtorrent_cap': rtorrent_cap, 'checked': len(torrents), 'excluded': len(excluded), 'settings': settings}