diff --git a/pytorrent/services/smart_queue.py b/pytorrent/services/smart_queue.py index be779b2..5d3480d 100644 --- a/pytorrent/services/smart_queue.py +++ b/pytorrent/services/smart_queue.py @@ -274,11 +274,25 @@ def _read_live_start_state(client: Any, torrent_hash: str) -> dict[str, Any]: result[key] = int(value or 0) if key in {'state', 'active', 'open', 'priority'} else str(value or '') except Exception as exc: result[f'{key}_error'] = str(exc) - # Note: Nie uznajemy d.is_open ani state=1 za wznowienie; Paused też potrafi mieć te wartości. - # Smart Queue zalicza start dopiero po d.is_active=1, czyli po realnym zdjęciu pauzy. + # Note: Realny slot liczymy po d.is_active=1. Dodatkowo zwracamy state/open/priority, + # bo przy masowym resume rTorrent czasem przyjmuje start, ale aktywuje transfer dopiero w kolejnym ticku. result['started'] = bool(int(result.get('active') or 0)) + result['start_accepted'] = bool(int(result.get('state') or 0) or int(result.get('open') or 0)) return result + +def _refresh_active_slots(profile: dict, excluded: set[str], manage_stopped: bool) -> tuple[int, list[dict[str, Any]]]: + """Read a fresh torrent snapshot and count real active Smart Queue slots.""" + fresh = rtorrent.list_torrents(profile) + active = [ + t for t in fresh + if str(t.get('hash') or '') not in excluded + and _is_running_download_slot(t) + ] + # Note: Po batchowym resume nie ufamy staremu snapshotowi; odświeżenie z rTorrent + # pozwala dobić kolejkę także wtedy, gdy aktywacja nastąpiła z opóźnieniem. + return len(active), fresh + def _set_smart_queue_label(client: Any, torrent_hash: str, attempts: int = 3) -> bool: for attempt in range(max(1, attempts)): try: @@ -508,18 +522,23 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool = 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 + max_resume_attempts = max(len(candidate_queue), max_active * 3) - # 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: + # Note: Resume działa w rundach aż do pełnego limitu z ustawień. Po każdej rundzie + # pobieramy świeży snapshot z rTorrent, bo masowe d.resume/d.start nie zawsze widać + # natychmiast w d.is_active na pojedynczym RPC. + while candidate_queue and active_slots < max_active and len(attempted_hashes) < max_resume_attempts: slots_left = max_active - active_slots - batch = candidate_queue[:slots_left] - candidate_queue = candidate_queue[slots_left:] + # Note: Bierzemy mały nadmiar kandydatów tylko wtedy, gdy poprzednie resume nie zwiększyło + # liczby aktywnych slotów; to naprawia przypadek, gdy część pauzowanych nie wstaje po komendzie. + batch_size = min(len(candidate_queue), max(1, slots_left)) + batch = candidate_queue[:batch_size] + candidate_queue = candidate_queue[batch_size:] batch_requested: list[str] = [] for t in batch: h = str(t.get('hash') or '') - if not h: + if not h or h in attempted_hashes: continue attempted_hashes.add(h) try: @@ -529,6 +548,10 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool = batch_requested.append(h) except Exception as exc: start_failed.append({'hash': h, 'error': str(exc)}) + time.sleep(0.03) + + if not batch_requested: + continue active_verified, batch_no_effect = _verify_started_downloads(c, batch_requested) start_no_effect.extend(batch_no_effect) @@ -536,10 +559,24 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool = 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 + fresh_active_slots, fresh_torrents = _refresh_active_slots(profile, excluded, manage_stopped) + active_slots = max(active_slots, fresh_active_slots) + + # Note: Jeżeli rTorrent wznowił torrent dopiero po odświeżeniu listy, dopisujemy go + # do resumed i zdejmujemy techniczny label Smart Queue. + fresh_by_hash = {str(t.get('hash') or ''): t for t in fresh_torrents} + for h in batch_requested: + live_t = fresh_by_hash.get(h) + if live_t and _is_running_download_slot(live_t) and h not in resumed: + _restore_auto_label(c, profile_id, h, None) + resumed.append(h) + + if active_slots < max_active and not candidate_queue: + # Note: Ostatnia próba dla pozycji, które przyjęły start, ale jeszcze nie pokazały active=1. + time.sleep(0.75) + fresh_active_slots, fresh_torrents = _refresh_active_slots(profile, excluded, manage_stopped) + active_slots = max(active_slots, fresh_active_slots) resumed_set = set(resumed) waiting_hashes = {