revert split js
This commit is contained in:
gru
2026-05-31 13:07:06 +02:00
parent b6a5003f2c
commit 6b8321e6e6
31 changed files with 253 additions and 148 deletions
+3 -3
View File
@@ -36,7 +36,7 @@ RECOMMENDED_TABLE_COLUMNS = {
"status": True, "size": True, "progress": True, "down_rate": True, "up_rate": True,
"eta": True, "seeds": True, "peers": True, "ratio": True, "path": True, "label": True,
"ratio_group": False, "down_total": True, "to_download": True, "up_total": True,
"created": False, "priority": False, "state": False, "active": False, "complete": False,
"created": False, "last_activity": False, "priority": False, "state": False, "active": False, "complete": False,
"hashing": False, "message": False, "hash": False,
},
"mobileSortFilters": {
@@ -48,7 +48,7 @@ RECOMMENDED_TABLE_COLUMNS = {
"down_rate": 60, "up_rate": 55, "eta": 53, "seeds": 44, "peers": 49,
"ratio": 47, "path": 135, "label": 67, "ratio_group": 87,
"down_total": 82, "to_download": 89, "up_total": 44, "created": 150,
"priority": 80, "state": 70, "active": 70, "complete": 82, "hashing": 82,
"last_activity": 150, "priority": 80, "state": 70, "active": 70, "complete": 82, "hashing": 82,
"message": 220, "hash": 280,
},
}
@@ -403,7 +403,7 @@ def save_profile_preferences(user_id: int, profile_id: int | None, data: dict) -
direction = int(parsed.get("dir") or 1)
except (TypeError, ValueError):
direction = 1
allowed_sort_keys = {"name", "status", "size", "progress", "down_rate", "up_rate", "eta", "seeds", "peers", "ratio", "path", "label", "ratio_group", "down_total", "to_download", "up_total", "created", "priority", "state", "active", "complete", "hashing", "message", "hash"}
allowed_sort_keys = {"name", "status", "size", "progress", "down_rate", "up_rate", "eta", "seeds", "peers", "ratio", "path", "label", "ratio_group", "down_total", "to_download", "up_total", "created", "last_activity", "priority", "state", "active", "complete", "hashing", "message", "hash"}
sort_key = str(parsed.get("key") or "name")
if sort_key not in allowed_sort_keys:
sort_key = "name"
+67 -13
View File
@@ -86,6 +86,15 @@ RTORRENT_CONFIG_FIELDS = [
"description": "Maximum simultaneous HTTP connections for tracker and metadata requests.",
"recommendation": "Moderate values reduce tracker pressure; increase only if tracker requests queue up.",
},
{
"group": "Network",
"key": "network.http.dns_cache_timeout",
"label": "HTTP DNS cache timeout",
"type": "number",
"description": "Seconds rTorrent keeps DNS results for tracker and HTTP requests.",
"recommendation": "Use a small positive value, for example 25, when many tracker hostnames are queried repeatedly.",
"runtime_note": "Applied through SCGI immediately; new HTTP lookups use the updated timeout.",
},
{
"group": "Network",
"key": "network.http.ssl_verify_peer",
@@ -162,34 +171,65 @@ RTORRENT_CONFIG_FIELDS = [
{
"group": "Throttle",
"key": "throttle.max_downloads.global",
"label": "Max active downloads",
"label": "Global download slots",
"type": "number",
"description": "Maximum number of downloading torrents active at once.",
"recommendation": "Match disk and network capacity; fewer active downloads often finish faster.",
"description": "Global number of peer download slots across all torrents; this is not the active torrent count.",
"recommendation": "Raise this on large instances so a few busy torrents do not starve the rest.",
"runtime_note": "Applied through SCGI immediately; existing peer scheduling catches up gradually.",
},
{
"group": "Throttle",
"key": "throttle.max_uploads.global",
"label": "Max active uploads",
"label": "Global upload slots",
"type": "number",
"description": "Maximum number of uploading torrents active at once.",
"recommendation": "Keep enough slots for ratio goals without overloading disks and sockets.",
"description": "Global number of peer upload slots across all torrents; this is not the active torrent count.",
"recommendation": "Keep enough slots for many seeds, but stay below socket and file descriptor limits.",
"runtime_note": "Applied through SCGI immediately; current peer connections may rebalance over time.",
},
{
"group": "Throttle",
"key": "throttle.max_downloads",
"label": "Per-torrent download slots",
"type": "number",
"description": "Maximum peer download slots allowed for a single torrent in the default throttle group.",
"recommendation": "Use values like 5-20 to prevent one torrent from consuming all global download slots.",
"runtime_note": "Applied through SCGI immediately; it affects new and rebalanced peer slot allocation.",
},
{
"group": "Throttle",
"key": "throttle.max_uploads",
"label": "Per-torrent upload slots",
"type": "number",
"description": "Maximum peer upload slots allowed for a single torrent in the default throttle group.",
"recommendation": "Use conservative values on very large seedboxes so many seeds can stay reachable.",
"runtime_note": "Applied through SCGI immediately; it affects new and rebalanced peer slot allocation.",
},
{
"group": "Throttle",
"key": "throttle.max_downloads.div",
"label": "Max downloads per throttle",
"label": "Download slot divisor",
"type": "number",
"description": "Per-throttle download slot divisor used by rTorrent throttling logic.",
"recommendation": "Change only when using named throttle groups or advanced queues.",
"recommendation": "Keep at 1 unless you intentionally use advanced throttle groups.",
"runtime_note": "Applied through SCGI immediately for the default throttle scheduler.",
},
{
"group": "Throttle",
"key": "throttle.max_uploads.div",
"label": "Max uploads per throttle",
"label": "Upload slot divisor",
"type": "number",
"description": "Per-throttle upload slot divisor used by rTorrent throttling logic.",
"recommendation": "Change only when using named throttle groups or advanced queues.",
"recommendation": "Keep at 1 unless you intentionally use advanced throttle groups.",
"runtime_note": "Applied through SCGI immediately for the default throttle scheduler.",
},
{
"group": "Ratio",
"key": "ratio.max",
"label": "Global ratio max",
"type": "number",
"description": "Global maximum ratio value used by rTorrent ratio logic where enabled.",
"recommendation": "Use -1 for no global cap, or manage per-profile ratio policies from pyTorrent when possible.",
"runtime_note": "Applied through SCGI immediately when the rTorrent ratio method is available.",
},
{
"group": "DHT / PEX",
@@ -410,6 +450,19 @@ def default_download_path(profile: dict) -> str:
errors.append(f"{method}: {exc}")
raise RuntimeError("Cannot read rTorrent default download directory: " + "; ".join(errors))
def _rtorrent_set_method(key: str, meta: dict) -> str:
# Note: Most runtime values use the conventional <method>.set setter.
# Some rTorrent commands, such as protocol.encryption.set, are already
# setter commands and must not receive another .set suffix.
return str(meta.get("set_method") or (key if key.endswith(".set") else f"{key}.set"))
def _rtorrent_config_line_key(key: str, meta: dict) -> str:
# Note: Generated snippets must match rTorrent config syntax and avoid
# producing invalid protocol.encryption.set.set lines.
return str(meta.get("config_key") or _rtorrent_set_method(key, meta))
def generate_config_text(values: dict) -> str:
known = {f["key"]: f for f in RTORRENT_CONFIG_FIELDS}
lines = []
@@ -420,7 +473,7 @@ def generate_config_text(values: dict) -> str:
normalized = _normalize_config_value(meta, value)
if meta.get("type") == "text" and any(ch.isspace() for ch in normalized):
normalized = '"' + normalized.replace('\\', '\\\\').replace('"', '\\"') + '"'
lines.append(f"{key}.set = {normalized}")
lines.append(f"{_rtorrent_config_line_key(key, meta)} = {normalized}")
return "\n".join(lines) + ("\n" if lines else "")
@@ -506,10 +559,11 @@ def set_config(profile: dict, values: dict, apply_now: bool = True, apply_on_sta
value = _normalize_config_value(meta, raw_value)
rpc_value = int(value) if meta.get("type") in {"bool", "number"} else value
try:
method = _rtorrent_set_method(key, meta)
try:
c.call(key + ".set", "", rpc_value)
c.call(method, "", rpc_value)
except Exception:
c.call(key + ".set", rpc_value)
c.call(method, rpc_value)
updated.append(key)
except Exception as exc:
errors.append({"key": key, "error": str(exc)})
+2 -3
View File
@@ -2,7 +2,6 @@ from __future__ import annotations
from .client import *
from .. import poller_control
import shlex
def scgi_diagnostics(profile: dict) -> dict:
c = client_for(profile)
@@ -90,12 +89,12 @@ def profile_diagnostics(profile: dict) -> dict:
base = paths.get("default_directory") if isinstance(paths.get("default_directory"), str) else ""
if base:
try:
out = _rt_execute(c, "execute.capture", "sh", "-lc", f"test -w {shlex.quote(base)} && printf writable || printf readonly")
out = _rt_execute(c, "execute.capture", "sh", "-c", 'if test -w "$1"; then printf writable; else printf readonly; fi', "pytorrent-diagnostics-write", base)
write_permissions[base] = str(out or "").strip() or "unknown"
except Exception as exc:
write_permissions[base] = f"error: {exc}"
try:
out = _rt_execute(c, "execute.capture", "sh", "-lc", f"df -Pk {shlex.quote(base)} | tail -1 | awk '{{print $4}}'")
out = _rt_execute(c, "execute.capture", "sh", "-c", "df -Pk \"$1\" 2>/dev/null | awk 'END {print $4}'", "pytorrent-diagnostics-df", base)
kb = int(str(out or "0").strip() or 0)
free_disk[base] = {"free_bytes": kb * 1024, "free_h": human_size(kb * 1024)}
except Exception as exc:
+10 -1
View File
@@ -1,5 +1,7 @@
from __future__ import annotations
import time
from .client import *
from .files import set_file_priorities
from .system import disk_usage_for_default_path
@@ -214,6 +216,7 @@ TORRENT_FIELDS = [
]
TORRENT_OPTIONAL_FIELDS = [
"d.timestamp.last_active=",
"d.timestamp.finished=",
]
@@ -252,7 +255,12 @@ def normalize_row(row: list) -> dict:
directory = str(row[14] or "")
base_path = str(row[15] or "")
is_multi_file = int(row[22] or 0) if len(row) > 22 else 0
completed_at = int(row[23] or 0) if len(row) > 23 else 0
# Note: Last activity is optional because older rTorrent builds may not expose this timestamp.
last_activity = int(row[23] or 0) if len(row) > 23 else 0
if not last_activity and (down_rate > 0 or up_rate > 0):
# Note: rTorrent builds without d.timestamp.last_active still expose live rates, so active rows get a safe current timestamp.
last_activity = int(time.time())
completed_at = int(row[24] or 0) if len(row) > 24 else 0
# Show the selected download location only. Hide the torrent root
# directory for multi-file torrents and the filename for single-file
@@ -310,6 +318,7 @@ def normalize_row(row: list) -> dict:
"priority": int(row[13] or 0),
"path": display_path,
"created": int(row[16] or 0),
"last_activity": last_activity,
"completed_at": completed_at,
"label": str(row[17] or ""),
"ratio_group": str(row[18] or ""),
+63 -14
View File
@@ -66,6 +66,8 @@ 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 ''),
@@ -155,6 +157,7 @@ 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(),
}
@@ -194,6 +197,8 @@ 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,
}
@@ -211,8 +216,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,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,prefer_partial_progress,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,
@@ -227,11 +232,12 @@ 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['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['prefer_partial_progress'], settings['auto_stop_idle'], settings['refill_enabled'], settings['refill_interval_minutes'], now),
)
return get_settings(profile_id, user_id)
@@ -800,9 +806,22 @@ def _is_running_download_slot(t: dict[str, Any]) -> bool:
return _is_started_download_slot(t)
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:
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:
"""Return True when a started torrent should begin or continue the stalled timer."""
# Note: Each ignore switch removes only its own criterion; the stalled timer still applies after criteria match.
# 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
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
@@ -810,13 +829,15 @@ 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: 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))}"
# 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))}"
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:
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:
"""Return True when a started torrent is weak and should be stopped first."""
# Note: Stop priority uses only criteria that are not ignored, so disabled criteria cannot stop torrents earlier.
# Note: Active transfers are never preferred for cleanup while non-transferring rows are available.
if _has_recent_transfer_activity(t, stalled_seconds):
return False
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))
@@ -842,6 +863,28 @@ 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.
@@ -932,9 +975,10 @@ 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: (int(t.get('seeds') or 0), int(t.get('peers') or 0), int(t.get('down_rate') or 0)),
key=lambda t: _start_candidate_sort_key(t, prefer_partial_progress),
reverse=True,
)
c = rtorrent.client_for(profile)
@@ -1026,6 +1070,7 @@ 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),
}
@@ -1058,6 +1103,7 @@ 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),
@@ -1232,9 +1278,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, ignore_seed_peer, ignore_speed)
is_stalled = _is_stalled_download(t, min_speed, min_seeds, min_peers, stalled_seconds, 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, ignore_seed_peer, ignore_speed):
if _is_low_activity_download(t, min_speed, min_seeds, min_peers, stalled_seconds, ignore_seed_peer, ignore_speed):
stop_eligible.append(t)
h = str(t.get('hash') or '')
if not h:
@@ -1259,9 +1305,10 @@ 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: (int(t.get('seeds') or 0), int(t.get('peers') or 0), int(t.get('down_rate') or 0)),
key=lambda t: _start_candidate_sort_key(t, prefer_partial_progress),
reverse=True,
)
max_active = max(1, int(settings.get('max_active_downloads') or 5))
@@ -1383,6 +1430,7 @@ 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),
@@ -1465,6 +1513,7 @@ 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),
@@ -1485,4 +1534,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, '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, '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}