smart queue fix
This commit is contained in:
@@ -1166,6 +1166,54 @@ def apply_startup_overrides(profile: dict) -> dict:
|
|||||||
return {"ok": True, "updated": [], "errors": [], "skipped": True}
|
return {"ok": True, "updated": [], "errors": [], "skipped": True}
|
||||||
return set_config(profile, values, apply_now=True, apply_on_start=True)
|
return set_config(profile, values, apply_now=True, apply_on_start=True)
|
||||||
|
|
||||||
|
|
||||||
|
def start_or_resume_hash(c: ScgiRtorrentClient, torrent_hash: str) -> dict:
|
||||||
|
"""Start stopped torrents and resume torrents paused with d.pause."""
|
||||||
|
h = str(torrent_hash or '')
|
||||||
|
if not h:
|
||||||
|
return {'hash': h, 'ok': False, 'error': 'missing hash'}
|
||||||
|
|
||||||
|
result: dict = {'hash': h, 'commands': []}
|
||||||
|
try:
|
||||||
|
result['state_before'] = int(c.call('d.state', h) or 0)
|
||||||
|
except Exception as exc:
|
||||||
|
result['state_before_error'] = str(exc)
|
||||||
|
result['state_before'] = 0
|
||||||
|
|
||||||
|
# Note: Ręczne Start i Smart Queue muszą zdejmować pause przez d.resume; samo d.start
|
||||||
|
# nie rusza torrentów zatrzymanych wcześniej komendą d.pause.
|
||||||
|
for method in ('d.resume',):
|
||||||
|
try:
|
||||||
|
c.call(method, h)
|
||||||
|
result['commands'].append(method)
|
||||||
|
except Exception as exc:
|
||||||
|
result.setdefault('ignored_errors', []).append(f'{method}: {exc}')
|
||||||
|
|
||||||
|
# Note: d.open bywa potrzebne po całkowitym stop/close; dla już otwartych torrentów jest bezpiecznie ignorowane.
|
||||||
|
try:
|
||||||
|
c.call('d.open', h)
|
||||||
|
result['commands'].append('d.open')
|
||||||
|
except Exception as exc:
|
||||||
|
result.setdefault('ignored_errors', []).append(f'd.open: {exc}')
|
||||||
|
|
||||||
|
for method in ('d.start', 'd.resume', 'd.try_start'):
|
||||||
|
try:
|
||||||
|
c.call(method, h)
|
||||||
|
result['commands'].append(method)
|
||||||
|
except Exception as exc:
|
||||||
|
result.setdefault('ignored_errors', []).append(f'{method}: {exc}')
|
||||||
|
|
||||||
|
try:
|
||||||
|
result['state_after'] = int(c.call('d.state', h) or 0)
|
||||||
|
except Exception as exc:
|
||||||
|
result['state_after_error'] = str(exc)
|
||||||
|
try:
|
||||||
|
result['active_after'] = int(c.call('d.is_active', h) or 0)
|
||||||
|
except Exception as exc:
|
||||||
|
result['active_after_error'] = str(exc)
|
||||||
|
result['ok'] = True
|
||||||
|
return result
|
||||||
|
|
||||||
def action(profile: dict, torrent_hashes: list[str], name: str, payload: dict | None = None) -> dict:
|
def action(profile: dict, torrent_hashes: list[str], name: str, payload: dict | None = None) -> dict:
|
||||||
payload = payload or {}
|
payload = payload or {}
|
||||||
c = client_for(profile)
|
c = client_for(profile)
|
||||||
@@ -1241,6 +1289,11 @@ def action(profile: dict, torrent_hashes: list[str], name: str, payload: dict |
|
|||||||
c.call("d.directory.set", h, path)
|
c.call("d.directory.set", h, path)
|
||||||
results.append(item)
|
results.append(item)
|
||||||
return {"ok": True, "count": len(torrent_hashes), "move_data": move_data, "results": results}
|
return {"ok": True, "count": len(torrent_hashes), "move_data": move_data, "results": results}
|
||||||
|
if name in {"start", "resume"}:
|
||||||
|
# Note: Start działa teraz także dla pozycji Paused, bo wykonuje pełną sekwencję resume/open/start.
|
||||||
|
results = [start_or_resume_hash(c, h) for h in torrent_hashes]
|
||||||
|
return {"ok": True, "count": len(torrent_hashes), "remove_data": False, "results": results}
|
||||||
|
|
||||||
method = methods.get(name)
|
method = methods.get(name)
|
||||||
if not method:
|
if not method:
|
||||||
raise ValueError(f"Unknown action: {name}")
|
raise ValueError(f"Unknown action: {name}")
|
||||||
|
|||||||
@@ -225,30 +225,17 @@ def _ensure_rtorrent_download_cap(client: Any, max_active: int) -> dict[str, Any
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
|
|
||||||
def _start_download(client: Any, torrent: dict[str, Any]) -> None:
|
def _start_download(client: Any, torrent: dict[str, Any]) -> dict[str, Any]:
|
||||||
"""Resume paused torrents and open/start stopped torrents with a tolerant RPC sequence."""
|
"""Resume paused torrents and open/start stopped torrents with the same path as manual Start."""
|
||||||
h = str(torrent.get('hash') or '')
|
h = str(torrent.get('hash') or '')
|
||||||
if not h:
|
if not h:
|
||||||
return
|
return {'hash': h, 'ok': False, 'error': 'missing hash'}
|
||||||
# Note: d.pause zostawia torrent w state=1, ale active=0; samo d.start często nic nie zmienia.
|
# Note: Smart Queue używa tej samej sekwencji co ręczny Start, żeby Paused nie zostawał w pauzie po samym d.start.
|
||||||
# Dlatego dla pozycji paused zawsze wysyłamy d.resume, a dla stopped próbujemy d.open przed d.start.
|
return rtorrent.start_or_resume_hash(client, h)
|
||||||
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]]]:
|
def _verify_started_downloads(client: Any, hashes: list[str], attempts: int = 10, delay: float = 0.5) -> tuple[list[str], list[dict[str, Any]]]:
|
||||||
"""Verify starts after a short scheduler delay instead of immediately after each RPC."""
|
"""Verify starts after rTorrent has time to process resume/start commands."""
|
||||||
pending = [h for h in hashes if h]
|
pending = [h for h in hashes if h]
|
||||||
started: list[str] = []
|
started: list[str] = []
|
||||||
no_effect: list[dict[str, Any]] = []
|
no_effect: list[dict[str, Any]] = []
|
||||||
@@ -287,9 +274,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', 'open', 'priority'} else str(value or '')
|
result[key] = int(value or 0) if key in {'state', 'active', 'open', 'priority'} else str(value or '')
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
result[f'{key}_error'] = str(exc)
|
result[f'{key}_error'] = str(exc)
|
||||||
# Note: Nie uznajemy samego state=1 za aktywny start, bo paused w rTorrent też potrafi mieć state=1.
|
# Note: Nie uznajemy d.is_open ani state=1 za wznowienie; Paused też potrafi mieć te wartości.
|
||||||
# Sukces techniczny Smart Queue jest zapisywany po zaakceptowanym RPC, a ta funkcja służy tylko do diagnostyki.
|
# Smart Queue zalicza start dopiero po d.is_active=1, czyli po realnym zdjęciu pauzy.
|
||||||
result['started'] = bool(int(result.get('active') or 0) or int(result.get('open') or 0))
|
result['started'] = bool(int(result.get('active') or 0))
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _set_smart_queue_label(client: Any, torrent_hash: str, attempts: int = 3) -> bool:
|
def _set_smart_queue_label(client: Any, torrent_hash: str, attempts: int = 3) -> bool:
|
||||||
@@ -369,9 +356,8 @@ def _cleanup_auto_labels(client: Any, profile_id: int, torrents: list[dict[str,
|
|||||||
|
|
||||||
def _is_running_download_slot(t: dict[str, Any]) -> bool:
|
def _is_running_download_slot(t: dict[str, Any]) -> bool:
|
||||||
"""Return True for incomplete torrents that already occupy a Smart Queue slot."""
|
"""Return True for incomplete torrents that already occupy a Smart Queue slot."""
|
||||||
# Note: Limit Smart Queue oznacza docelową liczbę realnie uruchomionych slotów.
|
# Note: Limit Smart Queue oznacza docelową liczbę realnie aktywnych slotów.
|
||||||
# rTorrent potrafi trzymać paused jako state=1, dlatego slot liczymy po state=1 tylko wtedy,
|
# Paused potrafi mieć state=1/open=1, dlatego slot liczymy dopiero po d.is_active=1.
|
||||||
# gdy torrent nie ma statusu Paused i nie jest oznaczony technicznym labelem Smart Queue.
|
|
||||||
if int(t.get('complete') or 0):
|
if int(t.get('complete') or 0):
|
||||||
return False
|
return False
|
||||||
if str(t.get('label') or '') == SMART_QUEUE_LABEL:
|
if str(t.get('label') or '') == SMART_QUEUE_LABEL:
|
||||||
@@ -379,7 +365,7 @@ def _is_running_download_slot(t: dict[str, Any]) -> bool:
|
|||||||
status = str(t.get('status') or '').lower()
|
status = str(t.get('status') or '').lower()
|
||||||
if status == 'checking' or status == 'paused' or bool(t.get('paused')):
|
if status == 'checking' or status == 'paused' or bool(t.get('paused')):
|
||||||
return False
|
return False
|
||||||
return bool(int(t.get('state') or 0))
|
return bool(int(t.get('active') or 0))
|
||||||
|
|
||||||
|
|
||||||
def _is_waiting_download_candidate(t: dict[str, Any], manage_stopped: bool) -> bool:
|
def _is_waiting_download_candidate(t: dict[str, Any], manage_stopped: bool) -> bool:
|
||||||
@@ -419,8 +405,7 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
|||||||
def is_managed_hold(t: dict[str, Any]) -> bool:
|
def is_managed_hold(t: dict[str, Any]) -> bool:
|
||||||
return str(t.get('label') or '') == SMART_QUEUE_LABEL
|
return str(t.get('label') or '') == SMART_QUEUE_LABEL
|
||||||
|
|
||||||
# Note: Slot Smart Queue liczymy po d.state, nie po d.is_active. d.is_active bywa 0
|
# Note: Slot Smart Queue liczymy po d.is_active, bo Paused może mieć state=1/open=1 i nie może zajmować miejsca w limicie.
|
||||||
# dla torrentu już wystartowanego, ale chwilowo bez transferu, więc powodował startowanie po jednej sztuce.
|
|
||||||
downloading = [
|
downloading = [
|
||||||
t for t in torrents
|
t for t in torrents
|
||||||
if _is_running_download_slot(t)
|
if _is_running_download_slot(t)
|
||||||
@@ -506,6 +491,7 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
|||||||
start_failed: list[dict[str, str]] = []
|
start_failed: list[dict[str, str]] = []
|
||||||
start_no_effect: list[dict[str, Any]] = []
|
start_no_effect: list[dict[str, Any]] = []
|
||||||
resume_requested: list[str] = []
|
resume_requested: list[str] = []
|
||||||
|
start_results: list[dict[str, Any]] = []
|
||||||
|
|
||||||
for t in to_pause:
|
for t in to_pause:
|
||||||
try:
|
try:
|
||||||
@@ -533,20 +519,23 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
|||||||
if not h:
|
if not h:
|
||||||
continue
|
continue
|
||||||
try:
|
try:
|
||||||
_start_download(c, t)
|
result = _start_download(c, t)
|
||||||
|
start_results.append(result)
|
||||||
resume_requested.append(h)
|
resume_requested.append(h)
|
||||||
_restore_auto_label(c, profile_id, h, None)
|
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
start_failed.append({'hash': h, 'error': str(exc)})
|
start_failed.append({'hash': h, 'error': str(exc)})
|
||||||
|
|
||||||
active_verified, start_no_effect = _verify_started_downloads(c, resume_requested)
|
active_verified, start_no_effect = _verify_started_downloads(c, resume_requested)
|
||||||
resumed = list(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 = (
|
keep_labels = (
|
||||||
set(paused)
|
set(paused)
|
||||||
| {str(t.get('hash') or '') for t in to_label_waiting}
|
| {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)}
|
| {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)
|
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, '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': 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}
|
||||||
add_history(profile_id, 'force_check' if force else 'auto_check', paused, resumed, len(torrents), details, user_id)
|
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(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}
|
||||||
|
|||||||
Reference in New Issue
Block a user