queue_stopped #3
@@ -433,7 +433,7 @@ 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: Target active downloads counts only actually active downloads; paused items are user-controlled.
|
# Note: Target active downloads counts the whole live queue, including torrents manually started by the user.
|
||||||
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 '')):
|
||||||
@@ -444,6 +444,15 @@ def _is_running_download_slot(t: dict[str, Any]) -> bool:
|
|||||||
return bool(int(t.get('active') or 0))
|
return bool(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:
|
||||||
|
"""Return True when an active torrent may be stopped to enforce Smart Queue limits."""
|
||||||
|
# Note: Manual starts are managed, but healthy downloads are not stopped only because the queue is above the limit.
|
||||||
|
low_speed = int(t.get('down_rate') or 0) <= max(0, int(min_speed or 0))
|
||||||
|
low_seeds = int(t.get('seeds') or 0) <= max(0, int(min_seeds or 0))
|
||||||
|
low_peers = int(t.get('peers') or 0) <= max(0, int(min_peers or 0)) if min_peers > 0 else False
|
||||||
|
return low_speed or low_seeds or low_peers
|
||||||
|
|
||||||
|
|
||||||
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 stopped torrents Smart Queue may start 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):
|
||||||
@@ -507,11 +516,15 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
|||||||
now = utcnow()
|
now = utcnow()
|
||||||
now_ts = datetime.now(timezone.utc).timestamp()
|
now_ts = datetime.now(timezone.utc).timestamp()
|
||||||
stalled: list[dict[str, Any]] = []
|
stalled: list[dict[str, Any]] = []
|
||||||
|
stop_eligible: list[dict[str, Any]] = []
|
||||||
|
|
||||||
with connect() as conn:
|
with connect() as conn:
|
||||||
for t in downloading:
|
for t in downloading:
|
||||||
# Note: Stalled detection requires low speed plus low seeds and, when configured, low peers.
|
# Note: Stalled detection keeps the existing conservative rule: low speed plus low seeds and, when configured, low peers.
|
||||||
is_stalled = int(t.get('down_rate') or 0) <= min_speed and int(t.get('seeds') or 0) <= min_seeds and (min_peers <= 0 or int(t.get('peers') or 0) <= min_peers)
|
is_stalled = int(t.get('down_rate') or 0) <= min_speed and int(t.get('seeds') or 0) <= min_seeds and (min_peers <= 0 or int(t.get('peers') or 0) <= min_peers)
|
||||||
|
# Note: Hard-limit enforcement may stop only inactive / no-source items, including manually started ones.
|
||||||
|
if _is_low_activity_download(t, min_speed, min_seeds, min_peers):
|
||||||
|
stop_eligible.append(t)
|
||||||
h = t.get('hash')
|
h = t.get('hash')
|
||||||
if not h:
|
if not h:
|
||||||
continue
|
continue
|
||||||
@@ -537,10 +550,11 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
|||||||
max_active = max(1, int(settings.get('max_active_downloads') or 5))
|
max_active = max(1, int(settings.get('max_active_downloads') or 5))
|
||||||
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 across the whole active queue, including manual starts.
|
||||||
# newly started torrents, so already-active downloads could stay above the limit.
|
# Note: Only low-activity/no-source downloads are eligible, so Smart Queue does not punish healthy manual starts.
|
||||||
|
over_limit = max(0, len(downloading) - max_active)
|
||||||
stop_rank = sorted(
|
stop_rank = sorted(
|
||||||
downloading,
|
stop_eligible,
|
||||||
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,
|
||||||
int(t.get('down_rate') or 0),
|
int(t.get('down_rate') or 0),
|
||||||
@@ -548,7 +562,7 @@ 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_stop: list[dict[str, Any]] = stop_rank[:max(0, len(downloading) - max_active)]
|
to_stop: list[dict[str, Any]] = stop_rank[:over_limit]
|
||||||
stop_hashes = {str(t.get('hash') or '') for t in to_stop}
|
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.
|
||||||
@@ -559,6 +573,7 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
|||||||
stop_hashes.add(h)
|
stop_hashes.add(h)
|
||||||
|
|
||||||
active_after_stop = max(0, len(downloading) - len(to_stop))
|
active_after_stop = max(0, len(downloading) - len(to_stop))
|
||||||
|
# Note: When healthy manual starts keep the queue above the limit, Smart Queue waits instead of starting more.
|
||||||
available_slots = max(0, max_active - active_after_stop)
|
available_slots = max(0, max_active - active_after_stop)
|
||||||
to_start = 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.
|
||||||
@@ -626,6 +641,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), '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(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': max(0, over_limit - len(to_stop)), '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, '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, 'over_limit': over_limit, 'stop_eligible': len(stop_eligible), 'healthy_active_protected': max(0, over_limit - len(to_stop)), 'rtorrent_cap': rtorrent_cap, 'checked': len(torrents), 'excluded': len(excluded), 'settings': settings}
|
||||||
|
|||||||
Reference in New Issue
Block a user