diff --git a/pytorrent/services/smart_queue.py b/pytorrent/services/smart_queue.py index 32078f3..84aacc6 100644 --- a/pytorrent/services/smart_queue.py +++ b/pytorrent/services/smart_queue.py @@ -557,10 +557,17 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool = now_ts = datetime.now(timezone.utc).timestamp() stalled: list[dict[str, Any]] = [] stop_eligible: list[dict[str, Any]] = [] + # Note: Toast diagnostics count active torrents whose ignored criteria would otherwise match during this check. + ignored_seed_peer_count = 0 + ignored_speed_count = 0 with connect() as conn: for t in downloading: # Note: Stalled detection respects seed/peer and speed ignore switches before starting the timer. + if ignore_seed_peer and (int(t.get('seeds') or 0) <= max(0, int(min_seeds or 0)) or (min_peers > 0 and int(t.get('peers') or 0) <= max(0, int(min_peers or 0)))): + ignored_seed_peer_count += 1 + if ignore_speed and int(t.get('down_rate') or 0) <= max(0, int(min_speed or 0)): + ignored_speed_count += 1 is_stalled = _is_stalled_download(t, min_speed, min_seeds, min_peers, ignore_seed_peer, ignore_speed) # Note: Hard-limit enforcement respects the same ignore switches before choosing weak items. if _is_low_activity_download(t, min_speed, min_seeds, min_peers, ignore_seed_peer, ignore_speed): @@ -691,6 +698,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)} ) restored = _cleanup_auto_labels(c, profile_id, torrents, keep_labels, manage_stopped) - 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), 'ignore_seed_peer': ignore_seed_peer, 'ignore_speed': ignore_speed, 'stalled_seconds': stalled_seconds, 'stalled_timer_key': timer_key, '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), 'ignore_seed_peer': ignore_seed_peer, 'ignore_speed': ignore_speed, 'ignored_seed_peer_count': ignored_seed_peer_count if ignore_seed_peer else 0, 'ignored_speed_count': ignored_speed_count if ignore_speed else 0, 'stalled_seconds': stalled_seconds, 'stalled_timer_key': timer_key, '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) - 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), 'ignore_seed_peer': ignore_seed_peer, 'ignore_speed': ignore_speed, 'stalled_seconds': stalled_seconds, 'stalled_timer_key': timer_key, 'healthy_active_protected': 0, 'rtorrent_cap': rtorrent_cap, 'checked': len(torrents), 'excluded': len(user_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), 'ignore_seed_peer': ignore_seed_peer, 'ignore_speed': ignore_speed, 'ignored_seed_peer_count': ignored_seed_peer_count if ignore_seed_peer else 0, 'ignored_speed_count': ignored_speed_count if ignore_speed else 0, 'stalled_seconds': stalled_seconds, 'stalled_timer_key': timer_key, 'healthy_active_protected': 0, 'rtorrent_cap': rtorrent_cap, 'checked': len(torrents), 'excluded': len(user_excluded), 'settings': settings} diff --git a/pytorrent/static/app.js b/pytorrent/static/app.js index 758edc5..893d85c 100644 --- a/pytorrent/static/app.js +++ b/pytorrent/static/app.js @@ -508,7 +508,7 @@ async function loadRss(){ const j=await (await fetch('/api/rss')).json(); const feeds=j.feeds||[], rules=j.rules||[]; if($('rssManager')) $('rssManager').innerHTML=`
Feeds
${table(['Name','URL','Last error'],feeds.map(f=>[esc(f.name),esc(f.url),esc(f.last_error||'')]))}
Rules
${table(['Name','Pattern','Path','Label'],rules.map(r=>[esc(r.name),esc(r.pattern),esc(r.save_path),esc(r.label)]))}`; } function smartHistoryDetails(row){ try{ return typeof row.details_json==='string'?JSON.parse(row.details_json||'{}'):(row.details_json||{}); }catch(e){ return {}; } } - function smartQueueToastMessage(r){ const noEffect=r.start_no_effect?.length||0; const requested=r.start_requested?.length||0; const stopFailed=r.stop_failed?.length||0; const limit=r.max_active_downloads||r.settings?.max_active_downloads||''; const activeBefore=r.active_before; const activeAfter=r.active_after_stop ?? r.active_after_expected; const activeTail=activeBefore!==undefined?`, active ${esc(activeBefore)}->${esc(activeAfter ?? '?')}${limit?`/${esc(limit)}`:''}`:''; const cap=r.rtorrent_cap?.updated?`, cap ${r.rtorrent_cap.current}->${r.rtorrent_cap.new}`:''; const waiting=r.waiting_labeled||0; const stalled=r.stalled_labeled?.length||0; const tail=noEffect?`, no effect ${noEffect}`:requested?`, requested ${requested}`:''; const waitTail=waiting?`, waiting labeled ${waiting}`:''; const stalledTail=stalled?`, stalled ${stalled}`:''; const failTail=stopFailed?`, stop failed ${stopFailed}`:''; return `Smart Queue: stopped ${r.stopped?.length||r.paused?.length||0}, started ${r.started?.length||r.resumed?.length||0}${activeTail}${tail}${waitTail}${stalledTail}${failTail}${cap}`; } + function smartQueueToastMessage(r){ const noEffect=r.start_no_effect?.length||0; const requested=r.start_requested?.length||0; const stopFailed=r.stop_failed?.length||0; const limit=r.max_active_downloads||r.settings?.max_active_downloads||''; const activeBefore=r.active_before; const activeAfter=r.active_after_stop ?? r.active_after_expected; const activeTail=activeBefore!==undefined?`, active ${esc(activeBefore)}->${esc(activeAfter ?? '?')}${limit?`/${esc(limit)}`:''}`:''; const cap=r.rtorrent_cap?.updated?`, cap ${r.rtorrent_cap.current}->${r.rtorrent_cap.new}`:''; const waiting=r.waiting_labeled||0; const stalled=r.stalled_labeled?.length||0; const ignoredSeedPeer=(r.ignore_seed_peer||r.settings?.ignore_seed_peer)?Number(r.ignored_seed_peer_count||0):0; const ignoredSpeed=(r.ignore_speed||r.settings?.ignore_speed)?Number(r.ignored_speed_count||0):0; const tail=noEffect?`, no effect ${noEffect}`:requested?`, requested ${requested}`:''; const waitTail=waiting?`, waiting labeled ${waiting}`:''; const stalledTail=stalled?`, stalled ${stalled}`:''; const ignoredSeedTail=(r.ignore_seed_peer||r.settings?.ignore_seed_peer)?`, ignored missing seeds/peers ${ignoredSeedPeer}`:''; const ignoredSpeedTail=(r.ignore_speed||r.settings?.ignore_speed)?`, ignored speed ${ignoredSpeed}`:''; const failTail=stopFailed?`, stop failed ${stopFailed}`:''; return `Smart Queue: stopped ${r.stopped?.length||r.paused?.length||0}, started ${r.started?.length||r.resumed?.length||0}${activeTail}${tail}${waitTail}${stalledTail}${ignoredSeedTail}${ignoredSpeedTail}${failTail}${cap}`; } function buildSmartQueueNerdStats(hist=[], totalHistory=0){ // Note: Small Smart Queue telemetry for automation nerds; it reads history only and does not affect queue behavior. const stats=hist.reduce((acc,h)=>{