fix in queue

This commit is contained in:
Mateusz Gruszczyński
2026-05-05 10:47:54 +02:00
parent 08772ddda5
commit 5874f8669d
5 changed files with 58 additions and 29 deletions

View File

@@ -29,6 +29,7 @@ def _default_settings(user_id: int, profile_id: int) -> dict[str, Any]:
'stalled_seconds': 300,
'min_speed_bytes': 1024,
'min_seeds': 1,
'manage_stopped': 0,
'updated_at': utcnow(),
}
@@ -52,20 +53,23 @@ def save_settings(profile_id: int, data: dict[str, Any], user_id: int | None = N
'stalled_seconds': max(30, int(data.get('stalled_seconds') or current.get('stalled_seconds') or 300)),
'min_speed_bytes': max(0, int(data.get('min_speed_bytes') or current.get('min_speed_bytes') or 0)),
'min_seeds': max(0, int(data.get('min_seeds') or current.get('min_seeds') or 0)),
# Note: Switch chroni całkiem zatrzymane torrenty przed automatycznym startem; domyślnie Smart Queue zarządza tylko paused.
'manage_stopped': 1 if data.get('manage_stopped', current.get('manage_stopped')) else 0,
}
now = utcnow()
with connect() as conn:
conn.execute(
'''INSERT INTO smart_queue_settings(user_id,profile_id,enabled,max_active_downloads,stalled_seconds,min_speed_bytes,min_seeds,updated_at)
VALUES(?,?,?,?,?,?,?,?)
'''INSERT INTO smart_queue_settings(user_id,profile_id,enabled,max_active_downloads,stalled_seconds,min_speed_bytes,min_seeds,manage_stopped,updated_at)
VALUES(?,?,?,?,?,?,?,?,?)
ON CONFLICT(user_id, profile_id) DO UPDATE SET
enabled=excluded.enabled,
max_active_downloads=excluded.max_active_downloads,
stalled_seconds=excluded.stalled_seconds,
min_speed_bytes=excluded.min_speed_bytes,
min_seeds=excluded.min_seeds,
manage_stopped=excluded.manage_stopped,
updated_at=excluded.updated_at''',
(user_id, profile_id, settings['enabled'], settings['max_active_downloads'], settings['stalled_seconds'], settings['min_speed_bytes'], settings['min_seeds'], now),
(user_id, profile_id, settings['enabled'], settings['max_active_downloads'], settings['stalled_seconds'], settings['min_speed_bytes'], settings['min_seeds'], settings['manage_stopped'], now),
)
return get_settings(profile_id, user_id)
@@ -195,10 +199,15 @@ def _mark_auto_paused(client: Any, profile_id: int, torrent: dict[str, Any]) ->
return _set_smart_queue_label(client, torrent_hash)
def _is_smart_queue_hold(torrent: dict[str, Any] | None) -> bool:
def _is_smart_queue_hold(torrent: dict[str, Any] | None, manage_stopped: bool = True) -> bool:
if not torrent or int(torrent.get('complete') or 0):
return False
return bool(torrent.get('paused')) or not int(torrent.get('active') or 0) or not int(torrent.get('state') or 0)
# Note: Gdy manage_stopped=False, techniczne labele Smart Queue dotyczą tylko paused, a nie całkiem zatrzymanych torrentów.
if bool(torrent.get('paused')):
return True
if not manage_stopped and not int(torrent.get('state') or 0):
return False
return not int(torrent.get('active') or 0) or not int(torrent.get('state') or 0)
def _clear_untracked_smart_queue_label(client: Any, torrent_hash: str, current_label: str) -> bool:
@@ -212,7 +221,7 @@ def _clear_untracked_smart_queue_label(client: Any, torrent_hash: str, current_l
return False
def _cleanup_auto_labels(client: Any, profile_id: int, torrents: list[dict[str, Any]], keep_hashes: set[str]) -> list[str]:
def _cleanup_auto_labels(client: Any, profile_id: int, torrents: list[dict[str, Any]], keep_hashes: set[str], manage_stopped: bool = True) -> list[str]:
by_hash = {str(t.get('hash') or ''): t for t in torrents}
restored: list[str] = []
with connect() as conn:
@@ -225,7 +234,7 @@ def _cleanup_auto_labels(client: Any, profile_id: int, torrents: list[dict[str,
if not h or h in keep_hashes:
continue
current_label = '' if t is None else str(t.get('label') or '')
if not _is_smart_queue_hold(t):
if not _is_smart_queue_hold(t, manage_stopped):
if _restore_auto_label(client, profile_id, h, None if t is None else current_label):
restored.append(h)
continue
@@ -233,7 +242,7 @@ def _cleanup_auto_labels(client: Any, profile_id: int, torrents: list[dict[str,
_set_smart_queue_label(client, h)
for h, t in by_hash.items():
if not h or h in keep_hashes or h in tracked_hashes or _is_smart_queue_hold(t):
if not h or h in keep_hashes or h in tracked_hashes or _is_smart_queue_hold(t, manage_stopped):
continue
if _clear_untracked_smart_queue_label(client, h, str(t.get('label') or '')):
restored.append(h)
@@ -252,7 +261,7 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
try:
# Note: Przy wyłączonym Smart Queue sprzątamy wyłącznie techniczne labele, bez startowania lub pauzowania torrentów.
torrents = rtorrent.list_torrents(profile)
restored = _cleanup_auto_labels(rtorrent.client_for(profile), profile_id, torrents, set())
restored = _cleanup_auto_labels(rtorrent.client_for(profile), profile_id, torrents, set(), bool(settings.get('manage_stopped')))
except Exception:
restored = []
add_history(profile_id, 'skipped_disabled', [], [], 0, {'enabled': False, 'labels_restored': restored}, user_id)
@@ -260,8 +269,15 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
torrents = rtorrent.list_torrents(profile)
excluded = _excluded_hashes(profile_id, user_id)
manage_stopped = bool(settings.get('manage_stopped'))
downloading = [t for t in torrents if not int(t.get('complete') or 0) and int(t.get('state') or 0) and not t.get('paused') and t.get('hash') not in excluded]
stopped = [t for t in torrents if not int(t.get('complete') or 0) and (not int(t.get('state') or 0) or t.get('paused')) and t.get('hash') not in excluded]
# Note: Domyślnie kolejka wznawia wyłącznie paused; stopped są kandydatami tylko po włączeniu switcha w ustawieniach.
stopped = [
t for t in torrents
if not int(t.get('complete') or 0)
and t.get('hash') not in excluded
and (bool(t.get('paused')) or (manage_stopped and not int(t.get('state') or 0)))
]
min_speed = int(settings.get('min_speed_bytes') or 0)
min_seeds = int(settings.get('min_seeds') or 0)
stalled_seconds = int(settings.get('stalled_seconds') or 300)
@@ -327,6 +343,7 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
paused: list[str] = []
resumed: list[str] = []
label_failed: list[str] = []
start_failed: list[dict[str, str]] = []
for t in to_pause:
try:
c.call('d.pause', t['hash'])
@@ -336,15 +353,17 @@ def check(profile: dict | None = None, user_id: int | None = None, force: bool =
except Exception:
pass
for t in to_resume:
h = t['hash']
try:
# Note: Wznowienie usuwa techniczny label przed startem i ponawia sprzątanie po starcie, gdy cache rTorrent jest opóźniony.
_restore_auto_label(c, profile_id, t['hash'], str(t.get('label') or ''))
c.call('d.resume', t['hash'])
c.call('d.start', t['hash'])
_restore_auto_label(c, profile_id, t['hash'], None)
resumed.append(t['hash'])
except Exception:
pass
restored = _cleanup_auto_labels(c, profile_id, torrents, set(paused))
add_history(profile_id, 'force_check' if force else 'auto_check', paused, resumed, len(torrents), {'excluded': len(excluded), 'enabled': bool(settings.get('enabled')), 'auto_label': SMART_QUEUE_LABEL, 'labels_restored': restored, 'labels_failed': label_failed, 'max_active_downloads': max_active, 'active_before': len(downloading), 'active_after': active_after_pause + len(resumed)}, user_id)
return {'ok': True, 'enabled': bool(settings.get('enabled')), 'paused': paused, 'resumed': resumed, 'labels_restored': restored, 'labels_failed': label_failed, 'checked': len(torrents), 'excluded': len(excluded), 'settings': settings}
# Note: Paused wymaga resume+start, a stopped startujemy bez wcześniejszego resume tylko wtedy, gdy switch na to pozwala.
_restore_auto_label(c, profile_id, h, str(t.get('label') or ''))
if bool(t.get('paused')):
c.call('d.resume', h)
c.call('d.start', h)
_restore_auto_label(c, profile_id, h, None)
resumed.append(h)
except Exception as exc:
start_failed.append({'hash': h, 'error': str(exc)})
restored = _cleanup_auto_labels(c, profile_id, torrents, set(paused), manage_stopped)
add_history(profile_id, 'force_check' if force else 'auto_check', paused, resumed, len(torrents), {'excluded': len(excluded), 'enabled': bool(settings.get('enabled')), 'auto_label': SMART_QUEUE_LABEL, 'labels_restored': restored, 'labels_failed': label_failed, 'start_failed': start_failed, 'manage_stopped': manage_stopped, 'max_active_downloads': max_active, 'active_before': len(downloading), 'active_after': active_after_pause + len(resumed)}, user_id)
return {'ok': True, 'enabled': bool(settings.get('enabled')), 'paused': paused, 'resumed': resumed, 'labels_restored': restored, 'labels_failed': label_failed, 'start_failed': start_failed, 'checked': len(torrents), 'excluded': len(excluded), 'settings': settings}

View File

@@ -1,5 +1,6 @@
from __future__ import annotations
import threading
import psutil
from flask_socketio import emit
from ..config import POLL_INTERVAL
@@ -9,10 +10,10 @@ from .torrent_summary import cached_summary
from . import rtorrent, smart_queue, traffic_history, automation_rules, torrent_stats
_started = False
_start_lock = threading.Lock()
def register_socketio_handlers(socketio):
global _started
def poller():
tick = 0
@@ -63,12 +64,19 @@ def register_socketio_handlers(socketio):
tick += 1
socketio.sleep(POLL_INTERVAL)
def ensure_poller_started():
global _started
with _start_lock:
if not _started:
# Note: Poller startuje przy starcie aplikacji, więc Smart Queue i automatyzacje działają bez otwartego UI.
socketio.start_background_task(poller)
_started = True
ensure_poller_started()
@socketio.on("connect")
def handle_connect():
global _started
if not _started:
socketio.start_background_task(poller)
_started = True
ensure_poller_started()
profile = active_profile()
emit("connected", {"ok": True, "profile": profile})
if not profile: