split js
This commit is contained in:
@@ -66,8 +66,6 @@ def _diagnostics_torrent(t: dict[str, Any] | None) -> dict[str, Any]:
|
||||
'hashing': int(t.get('hashing') or 0),
|
||||
'priority': int(t.get('priority') or 0),
|
||||
'down_rate': int(t.get('down_rate') or 0),
|
||||
'up_rate': int(t.get('up_rate') or 0),
|
||||
'last_activity': int(t.get('last_activity') or 0),
|
||||
'peers': int(t.get('peers') or 0),
|
||||
'seeds': int(t.get('seeds') or 0),
|
||||
'label': str(t.get('label') or ''),
|
||||
@@ -157,7 +155,6 @@ def _default_settings(profile_id: int) -> dict[str, Any]:
|
||||
'stop_batch_size': 50,
|
||||
'start_grace_seconds': 900,
|
||||
'protect_active_below_cap': 1,
|
||||
'prefer_partial_progress': 1,
|
||||
'auto_stop_idle': 0,
|
||||
'updated_at': utcnow(),
|
||||
}
|
||||
@@ -197,8 +194,6 @@ def save_settings(profile_id: int, data: dict[str, Any], user_id: int | None = N
|
||||
'start_grace_seconds': _int_setting(data, current, 'start_grace_seconds', 900, 0),
|
||||
# Note: When below the target cap, prefer refilling first instead of reducing active slots by stopping stalled downloads.
|
||||
'protect_active_below_cap': 1 if data.get('protect_active_below_cap', current.get('protect_active_below_cap', 1)) else 0,
|
||||
# Note: Prefer partially downloaded stopped torrents so Smart Queue finishes existing work before opening fresh downloads.
|
||||
'prefer_partial_progress': 1 if data.get('prefer_partial_progress', current.get('prefer_partial_progress', 1)) else 0,
|
||||
# Note: Optional safety valve that disables Smart Queue when there are no active or waiting downloads to manage.
|
||||
'auto_stop_idle': 1 if data.get('auto_stop_idle', current.get('auto_stop_idle', 0)) else 0,
|
||||
}
|
||||
@@ -216,8 +211,8 @@ def save_settings(profile_id: int, data: dict[str, Any], user_id: int | None = N
|
||||
now = utcnow()
|
||||
with connect() as conn:
|
||||
conn.execute(
|
||||
'''INSERT INTO smart_queue_settings(profile_id,enabled,max_active_downloads,stalled_seconds,min_speed_bytes,min_seeds,min_peers,ignore_seed_peer,ignore_speed,manage_stopped,cooldown_minutes,stop_batch_size,start_grace_seconds,protect_active_below_cap,prefer_partial_progress,auto_stop_idle,refill_enabled,refill_interval_minutes,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
'''INSERT INTO smart_queue_settings(profile_id,enabled,max_active_downloads,stalled_seconds,min_speed_bytes,min_seeds,min_peers,ignore_seed_peer,ignore_speed,manage_stopped,cooldown_minutes,stop_batch_size,start_grace_seconds,protect_active_below_cap,auto_stop_idle,refill_enabled,refill_interval_minutes,updated_at)
|
||||
VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)
|
||||
ON CONFLICT(profile_id) DO UPDATE SET
|
||||
enabled=excluded.enabled,
|
||||
max_active_downloads=excluded.max_active_downloads,
|
||||
@@ -232,12 +227,11 @@ def save_settings(profile_id: int, data: dict[str, Any], user_id: int | None = N
|
||||
stop_batch_size=excluded.stop_batch_size,
|
||||
start_grace_seconds=excluded.start_grace_seconds,
|
||||
protect_active_below_cap=excluded.protect_active_below_cap,
|
||||
prefer_partial_progress=excluded.prefer_partial_progress,
|
||||
auto_stop_idle=excluded.auto_stop_idle,
|
||||
refill_enabled=excluded.refill_enabled,
|
||||
refill_interval_minutes=excluded.refill_interval_minutes,
|
||||
updated_at=excluded.updated_at''',
|
||||
(profile_id, settings['enabled'], settings['max_active_downloads'], settings['stalled_seconds'], settings['min_speed_bytes'], settings['min_seeds'], settings['min_peers'], settings['ignore_seed_peer'], settings['ignore_speed'], settings['manage_stopped'], settings['cooldown_minutes'], settings['stop_batch_size'], settings['start_grace_seconds'], settings['protect_active_below_cap'], settings['prefer_partial_progress'], settings['auto_stop_idle'], settings['refill_enabled'], settings['refill_interval_minutes'], now),
|
||||
(profile_id, settings['enabled'], settings['max_active_downloads'], settings['stalled_seconds'], settings['min_speed_bytes'], settings['min_seeds'], settings['min_peers'], settings['ignore_seed_peer'], settings['ignore_speed'], settings['manage_stopped'], settings['cooldown_minutes'], settings['stop_batch_size'], settings['start_grace_seconds'], settings['protect_active_below_cap'], settings['auto_stop_idle'], settings['refill_enabled'], settings['refill_interval_minutes'], now),
|
||||
)
|
||||
return get_settings(profile_id, user_id)
|
||||
|
||||
@@ -806,22 +800,9 @@ def _is_running_download_slot(t: dict[str, Any]) -> bool:
|
||||
return _is_started_download_slot(t)
|
||||
|
||||
|
||||
def _has_recent_transfer_activity(t: dict[str, Any], stalled_seconds: int) -> bool:
|
||||
"""Return True when a torrent is currently transferring or was active within the stalled window."""
|
||||
# Note: Live transfer rates always protect a torrent from being marked as stalled.
|
||||
if int(t.get('down_rate') or 0) > 0 or int(t.get('up_rate') or 0) > 0:
|
||||
return True
|
||||
last_activity = int(t.get('last_activity') or 0)
|
||||
if last_activity <= 0:
|
||||
return False
|
||||
return time.time() - last_activity < max(1, int(stalled_seconds or 0))
|
||||
|
||||
|
||||
def _is_stalled_download(t: dict[str, Any], min_speed: int, min_seeds: int, min_peers: int, stalled_seconds: int, ignore_seed_peer: bool, ignore_speed: bool) -> bool:
|
||||
def _is_stalled_download(t: dict[str, Any], min_speed: int, min_seeds: int, min_peers: int, ignore_seed_peer: bool, ignore_speed: bool) -> bool:
|
||||
"""Return True when a started torrent should begin or continue the stalled timer."""
|
||||
# Note: Recent transfer activity wins over ignored source/speed criteria, preventing active torrents from being stopped as stalled.
|
||||
if _has_recent_transfer_activity(t, stalled_seconds):
|
||||
return False
|
||||
# Note: Each ignore switch removes only its own criterion; the stalled timer still applies after criteria match.
|
||||
speed_ok = True if ignore_speed else int(t.get('down_rate') or 0) <= max(0, int(min_speed or 0))
|
||||
source_ok = True if ignore_seed_peer else int(t.get('seeds') or 0) <= max(0, int(min_seeds or 0)) and (min_peers <= 0 or int(t.get('peers') or 0) <= min_peers)
|
||||
return speed_ok and source_ok
|
||||
@@ -829,15 +810,13 @@ def _is_stalled_download(t: dict[str, Any], min_speed: int, min_seeds: int, min_
|
||||
|
||||
def _stalled_timer_key(min_speed: int, min_seeds: int, min_peers: int, stalled_seconds: int, ignore_seed_peer: bool, ignore_speed: bool) -> str:
|
||||
"""Return a stable key for the stalled rules that started the current timer."""
|
||||
# Note: Version bump clears old timers created by the previous ignore-speed/source behavior.
|
||||
return f"v5|speed={int(min_speed or 0)}|seeds={int(min_seeds or 0)}|peers={int(min_peers or 0)}|seconds={int(stalled_seconds or 0)}|ignore_sources={int(bool(ignore_seed_peer))}|ignore_speed={int(bool(ignore_speed))}"
|
||||
# Note: Changing ignore switches or thresholds restarts existing stalled timers instead of reusing old rows.
|
||||
return f"v4|speed={int(min_speed or 0)}|seeds={int(min_seeds or 0)}|peers={int(min_peers or 0)}|seconds={int(stalled_seconds or 0)}|ignore_sources={int(bool(ignore_seed_peer))}|ignore_speed={int(bool(ignore_speed))}"
|
||||
|
||||
|
||||
def _is_low_activity_download(t: dict[str, Any], min_speed: int, min_seeds: int, min_peers: int, stalled_seconds: int, ignore_seed_peer: bool = False, ignore_speed: bool = False) -> bool:
|
||||
def _is_low_activity_download(t: dict[str, Any], min_speed: int, min_seeds: int, min_peers: int, ignore_seed_peer: bool = False, ignore_speed: bool = False) -> bool:
|
||||
"""Return True when a started torrent is weak and should be stopped first."""
|
||||
# Note: Active transfers are never preferred for cleanup while non-transferring rows are available.
|
||||
if _has_recent_transfer_activity(t, stalled_seconds):
|
||||
return False
|
||||
# Note: Stop priority uses only criteria that are not ignored, so disabled criteria cannot stop torrents earlier.
|
||||
low_speed = False if ignore_speed else int(t.get('down_rate') or 0) <= max(0, int(min_speed or 0))
|
||||
low_seeds = False if ignore_seed_peer else int(t.get('seeds') or 0) <= max(0, int(min_seeds or 0))
|
||||
low_peers = False if ignore_seed_peer or min_peers <= 0 else int(t.get('peers') or 0) <= max(0, int(min_peers or 0))
|
||||
@@ -863,28 +842,6 @@ def _is_waiting_download_candidate(t: dict[str, Any], manage_stopped: bool) -> b
|
||||
|
||||
|
||||
|
||||
def _progress_value(torrent: dict[str, Any]) -> float:
|
||||
"""Return a safe 0-100 progress value for queue ranking."""
|
||||
try:
|
||||
value = float(torrent.get('progress') or 0)
|
||||
except (TypeError, ValueError):
|
||||
return 0.0
|
||||
return max(0.0, min(100.0, value))
|
||||
|
||||
|
||||
def _start_candidate_sort_key(torrent: dict[str, Any], prefer_partial_progress: bool) -> tuple[float, float, int, int, int]:
|
||||
"""Rank stopped downloads for starting; partial progress can win so work is finished first."""
|
||||
progress = _progress_value(torrent)
|
||||
# Note: Existing partial downloads are preferred by default, then higher progress, then better source counts.
|
||||
partial_rank = 1.0 if prefer_partial_progress and 0.0 < progress < 100.0 else 0.0
|
||||
return (
|
||||
partial_rank,
|
||||
progress if prefer_partial_progress else 0.0,
|
||||
int(torrent.get('seeds') or 0),
|
||||
int(torrent.get('peers') or 0),
|
||||
int(torrent.get('down_rate') or 0),
|
||||
)
|
||||
|
||||
def _split_start_candidates(torrents: list[dict[str, Any]]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
|
||||
"""Return all stopped torrents as start candidates without relying on stale source counts."""
|
||||
# Note: rTorrent/tracker source counts can be missing before announce, so start decisions are not filtered by seeds or peers.
|
||||
@@ -975,10 +932,9 @@ def _refill_underfilled_queue(profile: dict, settings: dict[str, Any], profile_i
|
||||
return _disable_when_idle(profile_id, user_id, torrents, idle_details)
|
||||
available_slots = max(0, max_active - len(downloading))
|
||||
startable_stopped, source_skipped = _split_start_candidates(stopped)
|
||||
prefer_partial_progress = bool(int(settings.get('prefer_partial_progress', 1) or 0))
|
||||
candidates = sorted(
|
||||
startable_stopped,
|
||||
key=lambda t: _start_candidate_sort_key(t, prefer_partial_progress),
|
||||
key=lambda t: (int(t.get('seeds') or 0), int(t.get('peers') or 0), int(t.get('down_rate') or 0)),
|
||||
reverse=True,
|
||||
)
|
||||
c = rtorrent.client_for(profile)
|
||||
@@ -1070,7 +1026,6 @@ def _refill_underfilled_queue(profile: dict, settings: dict[str, Any], profile_i
|
||||
'labels_failed': label_failed,
|
||||
'labels_restored': restored,
|
||||
'max_active_downloads': max_active,
|
||||
'prefer_partial_progress': prefer_partial_progress,
|
||||
'excluded': len(user_excluded),
|
||||
'excluded_stalled': len(stalled_label_hashes),
|
||||
}
|
||||
@@ -1103,7 +1058,6 @@ def _refill_underfilled_queue(profile: dict, settings: dict[str, Any], profile_i
|
||||
'refill_interval_minutes': int(settings.get('refill_interval_minutes') or 0),
|
||||
'min_seeds': min_seeds,
|
||||
'min_peers': min_peers,
|
||||
'prefer_partial_progress': prefer_partial_progress,
|
||||
},
|
||||
'to_start': _diagnostics_torrents(to_start),
|
||||
'to_label_waiting': _diagnostics_torrents(to_label_waiting),
|
||||
@@ -1278,9 +1232,9 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
||||
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, stalled_seconds, ignore_seed_peer, ignore_speed)
|
||||
is_stalled = _is_stalled_download(t, min_speed, min_seeds, min_peers, ignore_seed_peer, ignore_speed)
|
||||
# Note: Hard-limit enforcement uses only non-ignored weak criteria before choosing weak items.
|
||||
if _is_low_activity_download(t, min_speed, min_seeds, min_peers, stalled_seconds, ignore_seed_peer, ignore_speed):
|
||||
if _is_low_activity_download(t, min_speed, min_seeds, min_peers, ignore_seed_peer, ignore_speed):
|
||||
stop_eligible.append(t)
|
||||
h = str(t.get('hash') or '')
|
||||
if not h:
|
||||
@@ -1305,10 +1259,9 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
||||
|
||||
# Note: Start candidates are not filtered by seeds/peers because those counts may be stale before announce.
|
||||
startable_stopped, source_skipped = _split_start_candidates(stopped)
|
||||
prefer_partial_progress = bool(int(settings.get('prefer_partial_progress', 1) or 0))
|
||||
candidates = sorted(
|
||||
startable_stopped,
|
||||
key=lambda t: _start_candidate_sort_key(t, prefer_partial_progress),
|
||||
key=lambda t: (int(t.get('seeds') or 0), int(t.get('peers') or 0), int(t.get('down_rate') or 0)),
|
||||
reverse=True,
|
||||
)
|
||||
max_active = max(1, int(settings.get('max_active_downloads') or 5))
|
||||
@@ -1430,7 +1383,6 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
||||
'enabled': bool(settings.get('enabled')),
|
||||
'checked': len(torrents),
|
||||
'max_active_downloads': max_active,
|
||||
'prefer_partial_progress': prefer_partial_progress,
|
||||
'active_before': len(downloading),
|
||||
'active_after_stop': active_after_stop,
|
||||
'active_after_expected': active_after_stop + len(started_by_queue),
|
||||
@@ -1513,7 +1465,6 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
||||
'start_grace_seconds': start_grace_seconds,
|
||||
'protect_active_below_cap': protect_active_below_cap,
|
||||
'auto_stop_idle': bool(int(settings.get('auto_stop_idle') or 0)),
|
||||
'prefer_partial_progress': prefer_partial_progress,
|
||||
},
|
||||
'rtorrent_cap': rtorrent_cap,
|
||||
'to_stop': _diagnostics_torrents(to_stop),
|
||||
@@ -1534,4 +1485,4 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
|
||||
mark_run(profile_id, user_id)
|
||||
settings = get_settings(profile_id, user_id)
|
||||
remaining = cooldown_remaining(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, 'start_batch_size': start_summary['start_batch_size'], 'start_verify_attempts': start_summary['start_verify_attempts'], 'start_verify_delay_seconds': start_summary['start_verify_delay_seconds'], '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, 'start_pending_confirmation': start_pending_confirmation, 'active_verified': active_verified, 'active_before': len(downloading), 'active_after_stop': active_after_stop, 'over_limit': over_limit, 'stop_eligible': len(stop_eligible), 'start_source_skipped': len(source_skipped), '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, 'stop_batch_size': stop_batch_size, 'start_grace_seconds': start_grace_seconds, 'protect_active_below_cap': protect_active_below_cap, 'prefer_partial_progress': prefer_partial_progress, 'auto_stop_idle': bool(int(settings.get('auto_stop_idle') or 0)), 'stalled_replacement_allowed': stalled_replacement_allowed, 'start_grace_protected': len(start_grace_hashes), 'replacement_capacity': replacement_capacity, 'protected_stalled': protected_stalled, 'healthy_active_protected': 0, 'rtorrent_cap': rtorrent_cap, 'checked': len(torrents), 'excluded': len(user_excluded), 'settings': settings, 'cooldown_remaining_seconds': remaining}
|
||||
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, 'start_batch_size': start_summary['start_batch_size'], 'start_verify_attempts': start_summary['start_verify_attempts'], 'start_verify_delay_seconds': start_summary['start_verify_delay_seconds'], '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, 'start_pending_confirmation': start_pending_confirmation, 'active_verified': active_verified, 'active_before': len(downloading), 'active_after_stop': active_after_stop, 'over_limit': over_limit, 'stop_eligible': len(stop_eligible), 'start_source_skipped': len(source_skipped), '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, 'stop_batch_size': stop_batch_size, 'start_grace_seconds': start_grace_seconds, 'protect_active_below_cap': protect_active_below_cap, 'auto_stop_idle': bool(int(settings.get('auto_stop_idle') or 0)), 'stalled_replacement_allowed': stalled_replacement_allowed, 'start_grace_protected': len(start_grace_hashes), 'replacement_capacity': replacement_capacity, 'protected_stalled': protected_stalled, 'healthy_active_protected': 0, 'rtorrent_cap': rtorrent_cap, 'checked': len(torrents), 'excluded': len(user_excluded), 'settings': settings, 'cooldown_remaining_seconds': remaining}
|
||||
|
||||
Reference in New Issue
Block a user