queue_stopped #3
@@ -298,6 +298,8 @@ def _start_download(client: Any, torrent: dict[str, Any]) -> dict[str, Any]:
|
|||||||
if _is_user_paused(torrent):
|
if _is_user_paused(torrent):
|
||||||
# Note: Smart Queue never unpauses user-paused torrents; it manages only stopped items.
|
# Note: Smart Queue never unpauses user-paused torrents; it manages only stopped items.
|
||||||
return {'hash': h, 'ok': False, 'skipped': 'user_paused'}
|
return {'hash': h, 'ok': False, 'skipped': 'user_paused'}
|
||||||
|
# Note: This is the same helper used by the manual Start action, so queue starts follow the UI path.
|
||||||
|
# Note: Smart Queue uses the same helper as the manual Start action, so start behavior stays identical.
|
||||||
return rtorrent.start_or_resume_hash(client, h)
|
return rtorrent.start_or_resume_hash(client, h)
|
||||||
|
|
||||||
|
|
||||||
@@ -379,9 +381,23 @@ def _mark_auto_stopped(client: Any, profile_id: int, torrent: dict[str, Any]) ->
|
|||||||
return _set_smart_queue_label(client, torrent_hash, previous)
|
return _set_smart_queue_label(client, torrent_hash, previous)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_started_download_slot(torrent: dict[str, Any] | None) -> bool:
|
||||||
|
"""Return True for incomplete torrents already started in rTorrent, including manual starts."""
|
||||||
|
if not torrent or int(torrent.get('complete') or 0):
|
||||||
|
return False
|
||||||
|
status = str(torrent.get('status') or '').lower()
|
||||||
|
if status == 'checking':
|
||||||
|
return False
|
||||||
|
# Note: Manual Start changes d.state first; d.is_active may stay 0 while rTorrent is queued or idle.
|
||||||
|
return bool(int(torrent.get('state') or 0) or int(torrent.get('active') or 0))
|
||||||
|
|
||||||
|
|
||||||
def _is_smart_queue_hold(torrent: dict[str, Any] | None, manage_stopped: bool = True) -> bool:
|
def _is_smart_queue_hold(torrent: dict[str, Any] | None, manage_stopped: bool = True) -> bool:
|
||||||
if not torrent or int(torrent.get('complete') or 0):
|
if not torrent or int(torrent.get('complete') or 0):
|
||||||
return False
|
return False
|
||||||
|
if _is_started_download_slot(torrent):
|
||||||
|
# Note: A manual start can leave the Smart Queue label behind; started items are active slots, not holds.
|
||||||
|
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):
|
if _is_user_paused(torrent):
|
||||||
@@ -434,16 +450,9 @@ 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 queue limit follows the same state that a manual Start changes: d.state=1.
|
# Note: Do not exclude Smart Queue/Stalled labels here. Manual Start can leave old labels,
|
||||||
# d.is_active only means currently active in the engine and misses started-but-idle torrents.
|
# and those torrents still must count toward the global Smart Queue limit.
|
||||||
if int(t.get('complete') or 0):
|
return _is_started_download_slot(t)
|
||||||
return False
|
|
||||||
if _has_smart_queue_label(str(t.get('label') or '')) or _has_stalled_label(str(t.get('label') or '')):
|
|
||||||
return False
|
|
||||||
status = str(t.get('status') or '').lower()
|
|
||||||
if status == 'checking':
|
|
||||||
return False
|
|
||||||
return bool(int(t.get('state') or 0) or int(t.get('active') or 0))
|
|
||||||
|
|
||||||
|
|
||||||
def _is_low_activity_download(t: dict[str, Any], min_speed: int, min_seeds: int, min_peers: int) -> bool:
|
def _is_low_activity_download(t: dict[str, Any], min_speed: int, min_seeds: int, min_peers: int) -> bool:
|
||||||
@@ -489,27 +498,29 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
|||||||
return {'ok': True, 'enabled': False, 'paused': [], 'resumed': [], 'stopped': [], 'started': [], '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: Stalled labels block automatic starting only; a manually started Stalled item still counts as a running slot.
|
||||||
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
|
user_excluded = _excluded_hashes(profile_id, user_id)
|
||||||
manage_stopped = True
|
manage_stopped = True
|
||||||
def is_managed_hold(t: dict[str, Any]) -> bool:
|
|
||||||
return _has_smart_queue_label(str(t.get('label') or ''))
|
|
||||||
|
|
||||||
# Note: Count Smart Queue slots by d.state because this is what manual Start changes in rTorrent.
|
# Note: Count every started incomplete torrent, including items started manually and items with old Smart Queue labels.
|
||||||
downloading = [
|
downloading = [
|
||||||
t for t in torrents
|
t for t in torrents
|
||||||
if _is_running_download_slot(t)
|
if _is_running_download_slot(t)
|
||||||
and not is_managed_hold(t)
|
and str(t.get('hash') or '') not in user_excluded
|
||||||
and t.get('hash') not in excluded
|
|
||||||
]
|
]
|
||||||
# Note: Waiting candidates are only stopped items; already-started/manual-started items are handled above.
|
# Note: Waiting candidates are stopped queue holds only; Stalled labels are not auto-started again.
|
||||||
stopped = [
|
stopped = [
|
||||||
t for t in torrents
|
t for t in torrents
|
||||||
if t.get('hash') not in excluded
|
if str(t.get('hash') or '') not in user_excluded
|
||||||
|
and str(t.get('hash') or '') not in stalled_label_hashes
|
||||||
and _is_waiting_download_candidate(t, manage_stopped)
|
and _is_waiting_download_candidate(t, manage_stopped)
|
||||||
and not _is_running_download_slot(t)
|
and not _is_running_download_slot(t)
|
||||||
]
|
]
|
||||||
|
manual_labeled_running = [
|
||||||
|
str(t.get('hash') or '') for t in downloading
|
||||||
|
if str(t.get('hash') or '') and _has_smart_queue_label(str(t.get('label') or ''))
|
||||||
|
]
|
||||||
min_speed = int(settings.get('min_speed_bytes') or 0)
|
min_speed = int(settings.get('min_speed_bytes') or 0)
|
||||||
min_seeds = int(settings.get('min_seeds') or 0)
|
min_seeds = int(settings.get('min_seeds') or 0)
|
||||||
min_peers = int(settings.get('min_peers') or 0)
|
min_peers = int(settings.get('min_peers') or 0)
|
||||||
@@ -575,30 +586,24 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
|||||||
to_stop.append(t)
|
to_stop.append(t)
|
||||||
stop_hashes.add(h)
|
stop_hashes.add(h)
|
||||||
|
|
||||||
active_after_stop = max(0, len(downloading) - len(to_stop))
|
|
||||||
# Note: After hard-cap enforcement, new starts use only real free slots.
|
|
||||||
available_slots = max(0, max_active - active_after_stop)
|
|
||||||
to_start = candidates[:available_slots]
|
|
||||||
# Note: Items outside the current start batch are explicitly marked as pending Smart Queue items.
|
|
||||||
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)
|
||||||
stopped_by_queue: list[str] = []
|
stopped_by_queue: list[str] = []
|
||||||
started_by_queue: list[str] = []
|
started_by_queue: list[str] = []
|
||||||
label_failed: list[str] = []
|
label_failed: list[str] = []
|
||||||
stalled_labeled: list[str] = []
|
stalled_labeled: list[str] = []
|
||||||
|
stop_failed: list[dict[str, 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]] = []
|
||||||
start_requested: list[str] = []
|
start_requested: list[str] = []
|
||||||
start_results: list[dict[str, Any]] = []
|
start_results: list[dict[str, Any]] = []
|
||||||
|
|
||||||
for t in to_stop:
|
for t in to_stop:
|
||||||
|
h = str(t.get('hash') or '')
|
||||||
try:
|
try:
|
||||||
h = str(t.get('hash') or '')
|
# Note: Smart Queue stops with the same low-level d.stop command used by the manual Stop action.
|
||||||
stop_result = rtorrent.stop_hash(c, h)
|
# This avoids extra pre-check RPCs and keeps large queues from failing after only a few items.
|
||||||
if not stop_result.get('ok'):
|
c.call('d.stop', h)
|
||||||
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)
|
||||||
@@ -607,8 +612,16 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
|||||||
elif not _mark_auto_stopped(c, profile_id, t):
|
elif not _mark_auto_stopped(c, profile_id, t):
|
||||||
label_failed.append(h)
|
label_failed.append(h)
|
||||||
stopped_by_queue.append(h)
|
stopped_by_queue.append(h)
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
pass
|
# Note: Stop failures are stored in history instead of being swallowed, so queue drift is visible.
|
||||||
|
stop_failed.append({'hash': h, 'error': str(exc)})
|
||||||
|
|
||||||
|
active_after_stop = max(0, len(downloading) - len(stopped_by_queue))
|
||||||
|
# Note: Starts are planned only after confirmed stops, so failed stops cannot push the queue above the cap.
|
||||||
|
available_slots = max(0, max_active - active_after_stop)
|
||||||
|
to_start = candidates[:available_slots]
|
||||||
|
# Note: Items outside the current start batch are explicitly marked as pending Smart Queue items.
|
||||||
|
to_label_waiting = candidates[available_slots:]
|
||||||
|
|
||||||
for t in to_label_waiting:
|
for t in to_label_waiting:
|
||||||
h = str(t.get('hash') or '')
|
h = str(t.get('hash') or '')
|
||||||
@@ -644,6 +657,6 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
|||||||
| {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)}
|
| {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, '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), 'over_limit': over_limit, 'stop_eligible': len(stop_eligible), 'healthy_active_protected': 0, 'stopped_planned': len(to_stop), 'started_planned': len(to_start), 'paused_planned': len(to_stop), 'resumed_planned': len(to_start), 'rtorrent_cap': rtorrent_cap}
|
details = {'excluded': len(user_excluded), 'excluded_stalled': len(stalled_label_hashes), 'manual_labeled_running': len(manual_labeled_running), 'manual_labeled_running_hashes': manual_labeled_running[:100], '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, 'stop_failed': stop_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_stop': active_after_stop, 'active_after_expected': active_after_stop + len(started_by_queue), 'over_limit': over_limit, 'stop_eligible': len(stop_eligible), 'healthy_active_protected': 0, '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', stopped_by_queue, started_by_queue, len(torrents), {**details, 'stopped': stopped_by_queue, 'started': started_by_queue}, 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': 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, 'over_limit': over_limit, 'stop_eligible': len(stop_eligible), 'healthy_active_protected': 0, '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), 'manual_labeled_running': len(manual_labeled_running), 'labels_restored': restored, 'labels_failed': label_failed, 'stop_failed': stop_failed, 'start_failed': start_failed, 'start_no_effect': start_no_effect, 'active_verified': active_verified, 'active_before': len(downloading), 'active_after_stop': active_after_stop, 'over_limit': over_limit, 'stop_eligible': len(stop_eligible), 'healthy_active_protected': 0, 'rtorrent_cap': rtorrent_cap, 'checked': len(torrents), 'excluded': len(user_excluded), 'settings': settings}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user