This commit is contained in:
Mateusz Gruszczyński
2026-05-07 22:40:03 +02:00
parent de7f9451b1
commit 10d25b88b1
7 changed files with 92 additions and 71 deletions

View File

@@ -25,4 +25,4 @@ PYTORRENT_TRAFFIC_HISTORY_RETENTION_DAYS=90
PYTORRENT_JOBS_RETENTION_DAYS=30 PYTORRENT_JOBS_RETENTION_DAYS=30
PYTORRENT_SMART_QUEUE_HISTORY_RETENTION_DAYS=30 PYTORRENT_SMART_QUEUE_HISTORY_RETENTION_DAYS=30
PYTORRENT_LOG_RETENTION_DAYS=30 PYTORRENT_LOG_RETENTION_DAYS=30
PYTORRENT_SMART_QUEUE_LABEL="Smart Queue Paused" PYTORRENT_SMART_QUEUE_LABEL="Smart Queue"

View File

@@ -70,5 +70,5 @@ TRAFFIC_HISTORY_RETENTION_DAYS = _env_int("PYTORRENT_TRAFFIC_HISTORY_RETENTION_D
JOBS_RETENTION_DAYS = _env_int("PYTORRENT_JOBS_RETENTION_DAYS", 30, 1) JOBS_RETENTION_DAYS = _env_int("PYTORRENT_JOBS_RETENTION_DAYS", 30, 1)
SMART_QUEUE_HISTORY_RETENTION_DAYS = _env_int("PYTORRENT_SMART_QUEUE_HISTORY_RETENTION_DAYS", 30, 1) SMART_QUEUE_HISTORY_RETENTION_DAYS = _env_int("PYTORRENT_SMART_QUEUE_HISTORY_RETENTION_DAYS", 30, 1)
LOG_RETENTION_DAYS = _env_int("PYTORRENT_LOG_RETENTION_DAYS", 30, 1) LOG_RETENTION_DAYS = _env_int("PYTORRENT_LOG_RETENTION_DAYS", 30, 1)
SMART_QUEUE_LABEL = os.getenv("PYTORRENT_SMART_QUEUE_LABEL", "Smart Queue Paused") SMART_QUEUE_LABEL = os.getenv("PYTORRENT_SMART_QUEUE_LABEL", "Smart Queue Stopped")
SMART_QUEUE_STALLED_LABEL = os.getenv("PYTORRENT_SMART_QUEUE_STALLED_LABEL", "Stalled") SMART_QUEUE_STALLED_LABEL = os.getenv("PYTORRENT_SMART_QUEUE_STALLED_LABEL", "Stalled")

View File

@@ -1216,6 +1216,27 @@ def pause_hash(c: ScgiRtorrentClient, torrent_hash: str) -> dict:
return result return result
def stop_hash(c: ScgiRtorrentClient, torrent_hash: str) -> dict:
"""Stop an active rTorrent item without using pause semantics."""
h = str(torrent_hash or '')
if not h:
return {'hash': h, 'ok': False, 'error': 'missing hash'}
before = _download_runtime_state(c, h)
result = {'hash': h, 'before': before, 'commands': []}
if before.get('stopped'):
result.update({'ok': True, 'skipped': 'already_stopped', 'after': before})
return result
try:
# Note: Smart Queue now enforces the queue with d.stop only; user-paused torrents stay untouched.
c.call('d.stop', h)
result['commands'].append('d.stop')
result['after'] = _download_runtime_state(c, h)
result['ok'] = True
except Exception as exc:
result.update({'ok': False, 'error': str(exc), 'after': _download_runtime_state(c, h)})
return result
def resume_paused_hash(c: ScgiRtorrentClient, torrent_hash: str) -> dict: def resume_paused_hash(c: ScgiRtorrentClient, torrent_hash: str) -> dict:
"""Resume only a paused rTorrent item; never convert it through stop/start.""" """Resume only a paused rTorrent item; never convert it through stop/start."""
h = str(torrent_hash or '') h = str(torrent_hash or '')

View File

@@ -38,7 +38,7 @@ def _default_settings(user_id: int, profile_id: int) -> dict[str, Any]:
'min_speed_bytes': 1024, 'min_speed_bytes': 1024,
'min_seeds': 1, 'min_seeds': 1,
'min_peers': 0, 'min_peers': 0,
'manage_stopped': 0, 'manage_stopped': 1,
'updated_at': utcnow(), 'updated_at': utcnow(),
} }
@@ -64,8 +64,8 @@ def save_settings(profile_id: int, data: dict[str, Any], user_id: int | None = N
'min_seeds': _int_setting(data, current, 'min_seeds', 0, 0), 'min_seeds': _int_setting(data, current, 'min_seeds', 0, 0),
# Note: Min peers is optional; when set, stalled detection requires low speed, low seeds and low peers. # Note: Min peers is optional; when set, stalled detection requires low speed, low seeds and low peers.
'min_peers': _int_setting(data, current, 'min_peers', 0, 0), 'min_peers': _int_setting(data, current, 'min_peers', 0, 0),
# Note: This switch protects fully stopped torrents from automatic starts; by default Smart Queue manages only paused items. # Note: Compatibility field retained; enabled Smart Queue always manages stopped torrents and never manages user-paused torrents.
'manage_stopped': 1 if data.get('manage_stopped', current.get('manage_stopped')) else 0, 'manage_stopped': 1,
} }
now = utcnow() now = utcnow()
with connect() as conn: with connect() as conn:
@@ -241,7 +241,7 @@ def _restore_auto_label(client: Any, profile_id: int, torrent_hash: str, current
except Exception: except Exception:
return False return False
try: try:
# Note: Starting a torrent removes only Smart Queue's technical marker, so labels added while paused stay untouched. # Note: Starting a torrent removes only Smart Queue's technical marker, so labels added while stopped stay untouched.
if _has_smart_queue_label(live_label): if _has_smart_queue_label(live_label):
client.call('d.custom1.set', torrent_hash, _without_smart_queue_label(live_label)) client.call('d.custom1.set', torrent_hash, _without_smart_queue_label(live_label))
conn.execute('DELETE FROM smart_queue_auto_labels WHERE profile_id=? AND torrent_hash=?', (profile_id, torrent_hash)) conn.execute('DELETE FROM smart_queue_auto_labels WHERE profile_id=? AND torrent_hash=?', (profile_id, torrent_hash))
@@ -291,14 +291,13 @@ def _ensure_rtorrent_download_cap(client: Any, max_active: int) -> dict[str, Any
def _start_download(client: Any, torrent: dict[str, Any]) -> dict[str, Any]: def _start_download(client: Any, torrent: dict[str, Any]) -> dict[str, Any]:
"""Resume paused torrents through rTorrent's pause model.""" """Start only stopped Smart Queue candidates; paused torrents are a user decision."""
h = str(torrent.get('hash') or '') h = str(torrent.get('hash') or '')
if not h: if not h:
return {'hash': h, 'ok': False, 'error': 'missing hash'} return {'hash': h, 'ok': False, 'error': 'missing hash'}
if bool(torrent.get('paused')) or str(torrent.get('status') or '').lower() == 'paused' or int(torrent.get('state') or 0): if _is_user_paused(torrent):
# Note: Smart Queue candidates paused with d.pause must be resumed with d.resume, without d.start/d.stop. # Note: Smart Queue never unpauses user-paused torrents; it manages only stopped items.
return rtorrent.resume_paused_hash(client, h) return {'hash': h, 'ok': False, 'skipped': 'user_paused'}
# Note: Only optional manage_stopped uses the start path for fully stopped torrents.
return rtorrent.start_or_resume_hash(client, h) return rtorrent.start_or_resume_hash(client, h)
@@ -342,11 +341,16 @@ 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: Do not treat d.is_open or state=1 as resumed; Paused can also have those values. # Note: Smart Queue starts only stopped torrents; a slot counts only after d.is_active=1.
# Smart Queue counts a start only after d.is_active=1, meaning the pause was actually removed.
result['started'] = bool(int(result.get('active') or 0)) result['started'] = bool(int(result.get('active') or 0))
return result return result
def _is_user_paused(torrent: dict[str, Any]) -> bool:
"""Return True for torrents paused by the user; Smart Queue must not touch them."""
status = str(torrent.get('status') or '').lower()
return bool(torrent.get('paused')) or status == 'paused'
def _set_smart_queue_label(client: Any, torrent_hash: str, current_label: str = '', attempts: int = 3) -> bool: def _set_smart_queue_label(client: Any, torrent_hash: str, current_label: str = '', attempts: int = 3) -> bool:
labels = _label_names(current_label) labels = _label_names(current_label)
if SMART_QUEUE_LABEL in labels: if SMART_QUEUE_LABEL in labels:
@@ -364,7 +368,7 @@ def _set_smart_queue_label(client: Any, torrent_hash: str, current_label: str =
return False return False
def _mark_auto_paused(client: Any, profile_id: int, torrent: dict[str, Any]) -> bool: def _mark_auto_stopped(client: Any, profile_id: int, torrent: dict[str, Any]) -> bool:
torrent_hash = str(torrent.get('hash') or '') torrent_hash = str(torrent.get('hash') or '')
if not torrent_hash: if not torrent_hash:
return False return False
@@ -379,15 +383,12 @@ def _is_smart_queue_hold(torrent: dict[str, Any] | None, manage_stopped: bool =
return False return False
if _has_stalled_label(str(torrent.get('label') or '')): if _has_stalled_label(str(torrent.get('label') or '')):
return False return False
if _is_user_paused(torrent):
# Note: Paused torrents are always treated as user-controlled and are not Smart Queue holds.
return False
if _has_smart_queue_label(str(torrent.get('label') or '')): if _has_smart_queue_label(str(torrent.get('label') or '')):
return True return True
# Note: Paused in rTorrent usually has state=1 and active=0, so state=0 must not be required. # Note: Smart Queue manages stopped torrents by default; the old manage_stopped flag is ignored for compatibility.
# This lets Smart Queue treat paused torrents as pending and fill the queue target later.
if bool(torrent.get('paused')):
return True
# Note: Fully stopped items are managed only when Use stopped torrents is enabled.
if not manage_stopped:
return False
return not int(torrent.get('state') or 0) return not int(torrent.get('state') or 0)
@@ -432,31 +433,30 @@ 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: The Smart Queue limit means the target number of actually active slots. # Note: Target active downloads counts only actually active downloads; paused items are user-controlled.
# Paused can have state=1/open=1, so a slot is counted only after d.is_active=1.
if int(t.get('complete') or 0): if int(t.get('complete') or 0):
return False return False
if _has_smart_queue_label(str(t.get('label') or '')) or _has_stalled_label(str(t.get('label') or '')): if _has_smart_queue_label(str(t.get('label') or '')) or _has_stalled_label(str(t.get('label') or '')):
return False return False
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 _is_user_paused(t):
return False return False
return bool(int(t.get('active') 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:
"""Return True for paused/held torrents Smart Queue may resume later.""" """Return True for stopped torrents Smart Queue may start later."""
if int(t.get('complete') or 0): if int(t.get('complete') or 0):
return False return False
if _has_stalled_label(str(t.get('label') or '')): if _has_stalled_label(str(t.get('label') or '')):
return False return False
if _is_user_paused(t):
# Note: User-paused torrents are never candidates, even when they have no Smart Queue label.
return False
if _has_smart_queue_label(str(t.get('label') or '')): if _has_smart_queue_label(str(t.get('label') or '')):
return True return True
# Note: Paused items are the primary source for filling the queue, regardless of manage_stopped. # Note: Enabled Smart Queue manages all stopped torrents; no separate stopped-torrent switch is needed.
if bool(t.get('paused')) or str(t.get('status') or '').lower() == 'paused': return not int(t.get('state') or 0)
return True
# Note: Stopped items are added only when the user enabled Use stopped torrents.
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]: def check(profile: dict | None = None, user_id: int | None = None, force: bool = False) -> dict[str, Any]:
@@ -471,17 +471,17 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
try: try:
# Note: When Smart Queue is disabled, only technical labels are cleaned up, without starting or pausing torrents. # Note: When Smart Queue is disabled, only technical labels are cleaned up, without starting or pausing torrents.
torrents = rtorrent.list_torrents(profile) torrents = rtorrent.list_torrents(profile)
restored = _cleanup_auto_labels(rtorrent.client_for(profile), profile_id, torrents, set(), bool(settings.get('manage_stopped'))) restored = _cleanup_auto_labels(rtorrent.client_for(profile), profile_id, torrents, set(), True)
except Exception: except Exception:
restored = [] restored = []
add_history(profile_id, 'skipped_disabled', [], [], 0, {'enabled': False, 'labels_restored': restored}, user_id) add_history(profile_id, 'skipped_disabled', [], [], 0, {'enabled': False, 'labels_restored': restored}, user_id)
return {'ok': True, 'enabled': False, 'paused': [], 'resumed': [], 'labels_restored': restored, 'message': 'Smart Queue disabled'} return {'ok': True, 'enabled': False, 'paused': [], 'resumed': [], 'stopped': [], 'started': [], 'labels_restored': restored, 'message': 'Smart Queue disabled'}
torrents = rtorrent.list_torrents(profile) torrents = rtorrent.list_torrents(profile)
# Note: Torrents marked as Stalled are treated as queue-blocked even when there are no other pending downloads. # Note: Torrents marked as Stalled are treated as queue-blocked even when there are no other pending downloads.
stalled_label_hashes = {str(t.get('hash') or '') for t in torrents if _has_stalled_label(str(t.get('label') or '')) and t.get('hash')} stalled_label_hashes = {str(t.get('hash') or '') for t in torrents if _has_stalled_label(str(t.get('label') or '')) and t.get('hash')}
excluded = _excluded_hashes(profile_id, user_id) | stalled_label_hashes excluded = _excluded_hashes(profile_id, user_id) | stalled_label_hashes
manage_stopped = bool(settings.get('manage_stopped')) manage_stopped = True
def is_managed_hold(t: dict[str, Any]) -> bool: def is_managed_hold(t: dict[str, Any]) -> bool:
return _has_smart_queue_label(str(t.get('label') or '')) return _has_smart_queue_label(str(t.get('label') or ''))
@@ -538,8 +538,8 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
stalled_hashes = {str(t.get('hash') or '') for t in stalled} stalled_hashes = {str(t.get('hash') or '') for t in stalled}
# Enforce the hard active-download cap first. The previous logic only limited # Enforce the hard active-download cap first. The previous logic only limited
# newly resumed torrents, so already-active downloads could stay above the limit. # newly started torrents, so already-active downloads could stay above the limit.
pause_rank = sorted( stop_rank = sorted(
downloading, downloading,
key=lambda t: ( key=lambda t: (
0 if str(t.get('hash') or '') in stalled_hashes else 1, 0 if str(t.get('hash') or '') in stalled_hashes else 1,
@@ -548,84 +548,84 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
int(t.get('peers') or 0), int(t.get('peers') or 0),
), ),
) )
to_pause: list[dict[str, Any]] = pause_rank[:max(0, len(downloading) - max_active)] to_stop: list[dict[str, Any]] = stop_rank[:max(0, len(downloading) - max_active)]
pause_hashes = {str(t.get('hash') or '') for t in to_pause} stop_hashes = {str(t.get('hash') or '') for t in to_stop}
# Note: Confirmed stalled downloads are removed from the active queue immediately, then new candidates can fill those slots. # Note: Confirmed stalled downloads are removed from the active queue immediately, then new candidates can fill those slots.
for t in stalled: for t in stalled:
h = str(t.get('hash') or '') h = str(t.get('hash') or '')
if h and h not in pause_hashes: if h and h not in stop_hashes:
to_pause.append(t) to_stop.append(t)
pause_hashes.add(h) stop_hashes.add(h)
active_after_pause = max(0, len(downloading) - len(to_pause)) active_after_stop = max(0, len(downloading) - len(to_stop))
available_slots = max(0, max_active - active_after_pause) available_slots = max(0, max_active - active_after_stop)
to_resume = candidates[:available_slots] to_start = candidates[:available_slots]
# Note: Items outside the current start batch are explicitly marked as pending Smart Queue items. # Note: Items outside the current start batch are explicitly marked as pending Smart Queue items.
to_label_waiting = candidates[available_slots:] to_label_waiting = candidates[available_slots:]
c = rtorrent.client_for(profile) c = rtorrent.client_for(profile)
rtorrent_cap = _ensure_rtorrent_download_cap(c, max_active) rtorrent_cap = _ensure_rtorrent_download_cap(c, max_active)
paused: list[str] = [] stopped_by_queue: list[str] = []
resumed: list[str] = [] started_by_queue: list[str] = []
label_failed: list[str] = [] label_failed: list[str] = []
stalled_labeled: list[str] = [] stalled_labeled: list[str] = []
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] = [] start_requested: list[str] = []
start_results: list[dict[str, Any]] = [] start_results: list[dict[str, Any]] = []
for t in to_pause: for t in to_stop:
try: try:
h = str(t.get('hash') or '') h = str(t.get('hash') or '')
pause_result = rtorrent.pause_hash(c, h) stop_result = rtorrent.stop_hash(c, h)
if not pause_result.get('ok'): if not stop_result.get('ok'):
raise RuntimeError(pause_result.get('error') or 'pause failed') raise RuntimeError(stop_result.get('error') or 'stop failed')
if h in stalled_hashes: if h in stalled_hashes:
if _ensure_stalled_label(c, h, _read_label(c, h, str(t.get('label') or ''))): if _ensure_stalled_label(c, h, _read_label(c, h, str(t.get('label') or ''))):
stalled_labeled.append(h) stalled_labeled.append(h)
else: else:
label_failed.append(h) label_failed.append(h)
elif not _mark_auto_paused(c, profile_id, t): elif not _mark_auto_stopped(c, profile_id, t):
label_failed.append(h) label_failed.append(h)
paused.append(h) stopped_by_queue.append(h)
except Exception: except Exception:
pass pass
for t in to_label_waiting: for t in to_label_waiting:
h = str(t.get('hash') or '') h = str(t.get('hash') or '')
if not h or h in pause_hashes: if not h or h in stop_hashes:
continue continue
try: try:
if not _mark_auto_paused(c, profile_id, t): if not _mark_auto_stopped(c, profile_id, t):
label_failed.append(h) label_failed.append(h)
except Exception: except Exception:
label_failed.append(h) label_failed.append(h)
# Note: Start the whole candidate batch in one round. Remove the label after an accepted RPC, # Note: Start the whole candidate batch in one round. Remove the label after an accepted RPC,
# because rTorrent may keep some items in its own queue with active=0 despite a valid d.start/d.resume. # because rTorrent may keep some items in its own queue with active=0 despite a valid d.start/d.resume.
for t in to_resume: for t in to_start:
h = str(t.get('hash') or '') h = str(t.get('hash') or '')
if not h: if not h:
continue continue
try: try:
result = _start_download(c, t) result = _start_download(c, t)
start_results.append(result) start_results.append(result)
resume_requested.append(h) start_requested.append(h)
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, start_requested)
for h in active_verified: for h in active_verified:
_restore_auto_label(c, profile_id, h, None) _restore_auto_label(c, profile_id, h, None)
# Note: History shows only torrents actually unpaused, not just the number of sent commands. # Note: History shows only torrents actually started, not just the number of sent commands.
resumed = list(active_verified) started_by_queue = list(active_verified)
keep_labels = ( keep_labels = (
set(paused) set(stopped_by_queue)
| {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 _has_smart_queue_label(str(t.get('label') or '')) and str(t.get('hash') or '') not in set(resumed)} | {str(t.get('hash') or '') for t in stopped if _has_smart_queue_label(str(t.get('label') or '')) and str(t.get('hash') or '') not in set(started_by_queue)}
) )
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), 'excluded_stalled': len(stalled_label_hashes), 'enabled': bool(settings.get('enabled')), 'auto_label': SMART_QUEUE_LABEL, 'stalled_label': SMART_QUEUE_STALLED_LABEL, 'stalled_labeled': stalled_labeled, '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), 'excluded_stalled': len(stalled_label_hashes), 'enabled': bool(settings.get('enabled')), 'auto_label': SMART_QUEUE_LABEL, 'stalled_label': SMART_QUEUE_STALLED_LABEL, 'stalled_labeled': stalled_labeled, 'labels_restored': restored, 'labels_failed': label_failed, 'start_failed': start_failed, 'start_no_effect': start_no_effect, 'start_results': start_results, 'start_requested': start_requested, 'active_verified': active_verified, 'waiting_labeled': len(to_label_waiting), 'manage_stopped': True, 'max_active_downloads': max_active, 'active_before': len(downloading), 'active_after_expected': active_after_stop + len(started_by_queue), 'stopped_planned': len(to_stop), 'started_planned': len(to_start), 'paused_planned': len(to_stop), 'resumed_planned': len(to_start), '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', stopped_by_queue, started_by_queue, len(torrents), {**details, 'stopped': stopped_by_queue, 'started': started_by_queue}, user_id)
return {'ok': True, 'enabled': bool(settings.get('enabled')), 'paused': paused, 'resumed': resumed, 'resume_requested': resume_requested, 'waiting_labeled': len(to_label_waiting), 'stalled_labeled': stalled_labeled, 'excluded_stalled': len(stalled_label_hashes), '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': stopped_by_queue, 'resumed': started_by_queue, 'stopped': stopped_by_queue, 'started': started_by_queue, 'start_requested': start_requested, 'waiting_labeled': len(to_label_waiting), 'stalled_labeled': stalled_labeled, 'excluded_stalled': len(stalled_label_hashes), '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}

View File

@@ -73,8 +73,8 @@ def register_socketio_handlers(socketio):
result = smart_queue.check(profile, force=False) result = smart_queue.check(profile, force=False)
if result.get("enabled"): if result.get("enabled"):
_emit_profile(socketio, "smart_queue_update", result, pid) _emit_profile(socketio, "smart_queue_update", result, pid)
if result.get("paused") or result.get("resumed") or result.get("resume_requested"): if result.get("stopped") or result.get("started") or result.get("start_requested") or result.get("paused") or result.get("resumed"):
# Note: After Smart Queue changes, refresh cache immediately so the Downloading list does not wait for the next poller cycle. # Note: Note: After Smart Queue STOP/START changes, refresh cache immediately so the Downloading list does not wait for the next poller cycle.
queue_diff = torrent_cache.refresh(profile) queue_diff = torrent_cache.refresh(profile)
if queue_diff.get("ok"): if queue_diff.get("ok"):
_emit_profile(socketio, "torrent_patch", {**queue_diff, "summary": cached_summary(pid, torrent_cache.snapshot(pid), force=True)}, pid) _emit_profile(socketio, "torrent_patch", {**queue_diff, "summary": cached_summary(pid, torrent_cache.snapshot(pid), force=True)}, pid)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long