Compare commits

...

2 Commits

Author SHA1 Message Date
Mateusz Gruszczyński 37d64079e9 fix ququq 2026-05-31 10:46:15 +02:00
Mateusz Gruszczyński 4fd18e3216 fix ququq 2026-05-31 10:37:37 +02:00
9 changed files with 84 additions and 24 deletions
+4 -2
View File
@@ -292,6 +292,7 @@ CREATE TABLE IF NOT EXISTS smart_queue_settings (
stop_batch_size INTEGER DEFAULT 50,
start_grace_seconds INTEGER DEFAULT 900,
protect_active_below_cap INTEGER DEFAULT 1,
prefer_partial_progress INTEGER DEFAULT 1,
auto_stop_idle INTEGER DEFAULT 0,
updated_at TEXT NOT NULL,
PRIMARY KEY(profile_id)
@@ -575,6 +576,7 @@ MIGRATIONS = [
"ALTER TABLE smart_queue_settings ADD COLUMN stop_batch_size INTEGER DEFAULT 50",
"ALTER TABLE smart_queue_settings ADD COLUMN start_grace_seconds INTEGER DEFAULT 900",
"ALTER TABLE smart_queue_settings ADD COLUMN protect_active_below_cap INTEGER DEFAULT 1",
"ALTER TABLE smart_queue_settings ADD COLUMN prefer_partial_progress INTEGER DEFAULT 1",
"ALTER TABLE smart_queue_settings ADD COLUMN auto_stop_idle INTEGER DEFAULT 0",
"CREATE TABLE IF NOT EXISTS smart_queue_start_grace (profile_id INTEGER NOT NULL, torrent_hash TEXT NOT NULL, started_at TEXT NOT NULL, updated_at TEXT NOT NULL, PRIMARY KEY(profile_id, torrent_hash))",
"ALTER TABLE rss_feeds ADD COLUMN interval_minutes INTEGER DEFAULT 30",
@@ -663,8 +665,8 @@ PROFILE_ONLY_TABLES = {
"indexes": ["CREATE INDEX IF NOT EXISTS idx_rss_history_profile_created ON rss_history(profile_id, created_at)", "CREATE INDEX IF NOT EXISTS idx_rss_history_profile_status ON rss_history(profile_id, status)", "CREATE UNIQUE INDEX IF NOT EXISTS idx_rss_history_unique_success ON rss_history(profile_id, COALESCE(rule_id,0), link) WHERE status IN ('queued','added')"],
},
"smart_queue_settings": {
"columns": "profile_id INTEGER NOT NULL, enabled INTEGER DEFAULT 0, max_active_downloads INTEGER DEFAULT 5, stalled_seconds INTEGER DEFAULT 300, min_speed_bytes INTEGER DEFAULT 1024, min_seeds INTEGER DEFAULT 1, min_peers INTEGER DEFAULT 0, ignore_seed_peer INTEGER DEFAULT 0, ignore_speed INTEGER DEFAULT 0, manage_stopped INTEGER DEFAULT 0, cooldown_minutes INTEGER DEFAULT 10, last_run_at TEXT, refill_enabled INTEGER DEFAULT 1, refill_interval_minutes INTEGER DEFAULT 0, last_refill_at TEXT, stop_batch_size INTEGER DEFAULT 50, start_grace_seconds INTEGER DEFAULT 900, protect_active_below_cap INTEGER DEFAULT 1, auto_stop_idle INTEGER DEFAULT 0, updated_at TEXT NOT NULL, PRIMARY KEY(profile_id)",
"copy": ["profile_id", "enabled", "max_active_downloads", "stalled_seconds", "min_speed_bytes", "min_seeds", "min_peers", "ignore_seed_peer", "ignore_speed", "manage_stopped", "cooldown_minutes", "last_run_at", "refill_enabled", "refill_interval_minutes", "last_refill_at", "stop_batch_size", "start_grace_seconds", "protect_active_below_cap", "auto_stop_idle", "updated_at"],
"columns": "profile_id INTEGER NOT NULL, enabled INTEGER DEFAULT 0, max_active_downloads INTEGER DEFAULT 5, stalled_seconds INTEGER DEFAULT 300, min_speed_bytes INTEGER DEFAULT 1024, min_seeds INTEGER DEFAULT 1, min_peers INTEGER DEFAULT 0, ignore_seed_peer INTEGER DEFAULT 0, ignore_speed INTEGER DEFAULT 0, manage_stopped INTEGER DEFAULT 0, cooldown_minutes INTEGER DEFAULT 10, last_run_at TEXT, refill_enabled INTEGER DEFAULT 1, refill_interval_minutes INTEGER DEFAULT 0, last_refill_at TEXT, stop_batch_size INTEGER DEFAULT 50, start_grace_seconds INTEGER DEFAULT 900, protect_active_below_cap INTEGER DEFAULT 1, prefer_partial_progress INTEGER DEFAULT 1, auto_stop_idle INTEGER DEFAULT 0, updated_at TEXT NOT NULL, PRIMARY KEY(profile_id)",
"copy": ["profile_id", "enabled", "max_active_downloads", "stalled_seconds", "min_speed_bytes", "min_seeds", "min_peers", "ignore_seed_peer", "ignore_speed", "manage_stopped", "cooldown_minutes", "last_run_at", "refill_enabled", "refill_interval_minutes", "last_refill_at", "stop_batch_size", "start_grace_seconds", "protect_active_below_cap", "prefer_partial_progress", "auto_stop_idle", "updated_at"],
"indexes": [],
},
"smart_queue_exclusions": {
+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}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long