smart queue fix
This commit is contained in:
@@ -181,34 +181,63 @@ def _restore_auto_label(client: Any, profile_id: int, torrent_hash: str, current
|
||||
|
||||
|
||||
|
||||
def _call_rtorrent_setter(client: Any, method: str, value: int) -> bool:
|
||||
"""Set a scalar rTorrent setting while tolerating XMLRPC signature differences."""
|
||||
for args in ((int(value),), ('', int(value))):
|
||||
try:
|
||||
client.call(method, *args)
|
||||
return True
|
||||
except Exception:
|
||||
continue
|
||||
return False
|
||||
|
||||
|
||||
def _ensure_rtorrent_download_cap(client: Any, max_active: int) -> dict[str, Any]:
|
||||
"""Raise rTorrent's own download cap when it is lower than Smart Queue's target."""
|
||||
result: dict[str, Any] = {'checked': False, 'updated': False}
|
||||
try:
|
||||
current = int(client.call('throttle.max_downloads.global') or 0)
|
||||
result.update({'checked': True, 'current': current, 'target': max_active})
|
||||
# Note: 0 means unlimited in rTorrent, so only smaller positive caps are raised.
|
||||
if 0 < current < max_active:
|
||||
try:
|
||||
client.call('throttle.max_downloads.global.set', '', int(max_active))
|
||||
except Exception:
|
||||
client.call('throttle.max_downloads.global.set', int(max_active))
|
||||
result.update({'updated': True, 'new': int(max_active)})
|
||||
except Exception as exc:
|
||||
# Note: Missing/older rTorrent throttle RPC should not block queue processing.
|
||||
result.update({'error': str(exc)})
|
||||
"""Raise rTorrent download caps that can silently limit Smart Queue to one item."""
|
||||
result: dict[str, Any] = {'checked': False, 'updated': False, 'items': []}
|
||||
# Note: rTorrent może mieć osobny limit globalny i per-throttle. Gdy div=1,
|
||||
# startowanie kończy się praktycznie jednym aktywnym torrentem mimo targetu 100.
|
||||
for key in ('throttle.max_downloads.global', 'throttle.max_downloads.div'):
|
||||
item: dict[str, Any] = {'key': key, 'checked': False, 'updated': False}
|
||||
try:
|
||||
current = int(client.call(key) or 0)
|
||||
item.update({'checked': True, 'current': current, 'target': int(max_active)})
|
||||
result['checked'] = True
|
||||
# Note: 0 oznacza unlimited; podnosimy tylko dodatnie limity niższe od targetu.
|
||||
if 0 < current < max_active:
|
||||
ok = _call_rtorrent_setter(client, f'{key}.set', int(max_active))
|
||||
item['updated'] = ok
|
||||
if ok:
|
||||
result['updated'] = True
|
||||
item['new'] = int(max_active)
|
||||
result.setdefault('current', current)
|
||||
result['new'] = int(max_active)
|
||||
except Exception as exc:
|
||||
item.update({'error': str(exc)})
|
||||
result['items'].append(item)
|
||||
return result
|
||||
|
||||
|
||||
def _start_download(client: Any, torrent: dict[str, Any]) -> None:
|
||||
"""Resume paused torrents and start stopped torrents using the smallest safe RPC sequence."""
|
||||
"""Resume paused torrents and open/start stopped torrents with a tolerant RPC sequence."""
|
||||
h = str(torrent.get('hash') or '')
|
||||
if not h:
|
||||
return
|
||||
# Note: Paused wymaga resume+start; stopped startujemy bez resume, gdy ustawienie na to pozwala.
|
||||
if bool(torrent.get('paused')):
|
||||
# Note: d.pause zostawia torrent w state=1, ale active=0; samo d.start często nic nie zmienia.
|
||||
# Dlatego dla pozycji paused zawsze wysyłamy d.resume, a dla stopped próbujemy d.open przed d.start.
|
||||
if bool(torrent.get('paused')) or int(torrent.get('state') or 0):
|
||||
client.call('d.resume', h)
|
||||
else:
|
||||
try:
|
||||
client.call('d.open', h)
|
||||
except Exception:
|
||||
pass
|
||||
client.call('d.start', h)
|
||||
if bool(torrent.get('paused')):
|
||||
try:
|
||||
client.call('d.resume', h)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _verify_started_downloads(client: Any, hashes: list[str], attempts: int = 3, delay: float = 0.25) -> tuple[list[str], list[dict[str, Any]]]:
|
||||
@@ -243,9 +272,9 @@ def _read_live_start_state(client: Any, torrent_hash: str) -> dict[str, Any]:
|
||||
result[key] = int(value or 0) if key in {'state', 'active'} else str(value or '')
|
||||
except Exception as exc:
|
||||
result[f'{key}_error'] = str(exc)
|
||||
# Note: rTorrent d.is_active oznacza realny transfer/aktywne peery, a nie slot kolejki.
|
||||
# Torrenty uruchomione, ale czekajace na peery/throttle, maja state=1 i active=0.
|
||||
result['started'] = bool(int(result.get('state') or 0))
|
||||
# Note: Dla Smart Queue slot aktywny musi zniknąć z UI jako Paused, więc wymagamy active=1.
|
||||
# state=1 alone może oznaczać nadal zapauzowany torrent po d.pause.
|
||||
result['started'] = bool(int(result.get('state') or 0)) and bool(int(result.get('active') or 0))
|
||||
return result
|
||||
|
||||
def _set_smart_queue_label(client: Any, torrent_hash: str, attempts: int = 3) -> bool:
|
||||
@@ -321,6 +350,27 @@ def _cleanup_auto_labels(client: Any, profile_id: int, torrents: list[dict[str,
|
||||
return restored
|
||||
|
||||
|
||||
def _is_running_download_slot(t: dict[str, Any]) -> bool:
|
||||
"""Return True only for torrents that occupy a visible active download slot."""
|
||||
# Note: normalize_row oznacza state=1/active=0 jako Paused; takich nie liczymy jako aktywne sloty.
|
||||
return (
|
||||
not int(t.get('complete') or 0)
|
||||
and int(t.get('state') or 0)
|
||||
and not bool(t.get('paused'))
|
||||
)
|
||||
|
||||
|
||||
def _is_waiting_download_candidate(t: dict[str, Any], manage_stopped: bool) -> bool:
|
||||
"""Return True for paused/held torrents Smart Queue may resume later."""
|
||||
if int(t.get('complete') or 0):
|
||||
return False
|
||||
if str(t.get('label') or '') == SMART_QUEUE_LABEL:
|
||||
return True
|
||||
if bool(t.get('paused')):
|
||||
return True
|
||||
return bool(manage_stopped) and not int(t.get('state') or 0)
|
||||
|
||||
|
||||
def check(profile: dict | None = None, user_id: int | None = None, force: bool = False) -> dict[str, Any]:
|
||||
profile = profile or active_profile()
|
||||
if not profile:
|
||||
@@ -349,18 +399,17 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
||||
# dla torrentu juz wystartowanego, ale chwilowo bez transferu, wiec powodowal startowanie po jednej sztuce.
|
||||
downloading = [
|
||||
t for t in torrents
|
||||
if not int(t.get('complete') or 0)
|
||||
and int(t.get('state') or 0)
|
||||
if _is_running_download_slot(t)
|
||||
and not is_managed_hold(t)
|
||||
and t.get('hash') not in excluded
|
||||
]
|
||||
# Note: Kandydaci do uruchomienia to przede wszystkim torrenty odlozone przez Smart Queue.
|
||||
# Nie traktujemy kazdego state=1/active=0 jako pauzy, bo rTorrent tak pokazuje tez oczekiwanie na peery/throttle.
|
||||
# Note: Kandydaci obejmują także zwykłe Paused bez labela. Inaczej kolejka widzi tylko 1-2 sztuki
|
||||
# i nie potrafi dobić do zadanego targetu 100.
|
||||
stopped = [
|
||||
t for t in torrents
|
||||
if not int(t.get('complete') or 0)
|
||||
and t.get('hash') not in excluded
|
||||
and (is_managed_hold(t) or (manage_stopped and not int(t.get('state') or 0)))
|
||||
if t.get('hash') not in excluded
|
||||
and _is_waiting_download_candidate(t, manage_stopped)
|
||||
and not _is_running_download_slot(t)
|
||||
]
|
||||
min_speed = int(settings.get('min_speed_bytes') or 0)
|
||||
min_seeds = int(settings.get('min_seeds') or 0)
|
||||
@@ -422,6 +471,8 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
||||
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)
|
||||
@@ -441,13 +492,22 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
for t in to_label_waiting:
|
||||
h = str(t.get('hash') or '')
|
||||
if not h or h in pause_hashes:
|
||||
continue
|
||||
try:
|
||||
if not _mark_auto_paused(c, profile_id, t):
|
||||
label_failed.append(h)
|
||||
except Exception:
|
||||
label_failed.append(h)
|
||||
|
||||
# Note: Startujemy całą pulę kandydatów w jednej rundzie, a dopiero potem weryfikujemy efekt.
|
||||
for t in to_resume:
|
||||
h = str(t.get('hash') or '')
|
||||
if not h:
|
||||
continue
|
||||
try:
|
||||
_restore_auto_label(c, profile_id, h, str(t.get('label') or ''))
|
||||
_start_download(c, t)
|
||||
resume_requested.append(h)
|
||||
except Exception as exc:
|
||||
@@ -457,8 +517,12 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
||||
for h in verified:
|
||||
_restore_auto_label(c, profile_id, h, None)
|
||||
resumed = verified
|
||||
keep_labels = set(paused) | {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)}
|
||||
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)}
|
||||
)
|
||||
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, 'resume_requested': resume_requested, 'manage_stopped': manage_stopped, 'max_active_downloads': max_active, 'active_before': len(downloading), 'active_after': active_after_pause + len(resumed), '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, 'resume_requested': resume_requested, 'waiting_labeled': len(to_label_waiting), 'manage_stopped': manage_stopped, 'max_active_downloads': max_active, 'active_before': len(downloading), 'active_after': active_after_pause + len(resumed), '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, 'labels_restored': restored, 'labels_failed': label_failed, 'start_failed': start_failed, 'start_no_effect': start_no_effect, '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(to_label_waiting), 'labels_restored': restored, 'labels_failed': label_failed, 'start_failed': start_failed, 'start_no_effect': start_no_effect, 'rtorrent_cap': rtorrent_cap, 'checked': len(torrents), 'excluded': len(excluded), 'settings': settings}
|
||||
|
||||
Reference in New Issue
Block a user