from __future__ import annotations import threading import psutil from flask_socketio import emit from ..config import POLL_INTERVAL from .preferences import active_profile, get_profile from .torrent_cache import torrent_cache 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): def poller(): tick = 0 while True: profile = active_profile() if profile: diff = torrent_cache.refresh(profile) heartbeat = {"ok": bool(diff.get("ok")), "profile_id": profile["id"], "tick": tick, "error": diff.get("error", "")} if diff.get("ok") and (diff["added"] or diff["updated"] or diff["removed"]): socketio.emit("torrent_patch", {**diff, "summary": cached_summary(profile["id"], torrent_cache.snapshot(profile["id"]), force=True)}) elif not diff.get("ok"): socketio.emit("rtorrent_error", diff) try: status = rtorrent.system_status(profile) if bool(profile.get("is_remote")): status["usage_source"] = "remote-hidden" status["usage_available"] = False else: status["cpu"] = psutil.cpu_percent(interval=None) status["ram"] = psutil.virtual_memory().percent status["usage_source"] = "local" status["usage_available"] = True status["profile_id"] = profile["id"] traffic_history.record(profile["id"], status.get("down_rate", 0), status.get("up_rate", 0), status.get("total_down", 0), status.get("total_up", 0)) socketio.emit("system_stats", status) heartbeat["ok"] = True except Exception as exc: heartbeat["ok"] = False heartbeat["error"] = str(exc) socketio.emit("rtorrent_error", {"profile_id": profile["id"], "error": str(exc)}) if tick % max(1, int(15 * 60 / POLL_INTERVAL)) == 0: # Note: Queue heavier torrent statistics outside the fast system_stats poller. torrent_stats.queue_refresh(socketio, profile, force=False) if tick % max(1, int(30 / POLL_INTERVAL)) == 0: try: result = smart_queue.check(profile, force=False) if result.get("enabled"): socketio.emit("smart_queue_update", result) except Exception as exc: socketio.emit("smart_queue_update", {"ok": False, "error": str(exc)}) try: auto_result = automation_rules.check(profile, force=False) if auto_result.get("applied"): socketio.emit("automation_update", auto_result) except Exception as exc: socketio.emit("automation_update", {"ok": False, "error": str(exc)}) socketio.emit("heartbeat", heartbeat) 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(): ensure_poller_started() profile = active_profile() emit("connected", {"ok": True, "profile": profile}) if not profile: # Note: Fresh installs have no rTorrent yet; tell the client to show setup instead of waiting for a snapshot. emit("profile_required", {"ok": True, "profiles": []}) return rows = torrent_cache.snapshot(profile["id"]) emit("torrent_snapshot", {"profile_id": profile["id"], "torrents": rows, "summary": cached_summary(profile["id"], rows)}) @socketio.on("select_profile") def handle_select_profile(data): profile_id = int((data or {}).get("profile_id") or 0) if not profile_id: # Note: Ignore empty profile selections created before the first rTorrent profile exists. emit("profile_required", {"ok": True, "profiles": []}) return profile = get_profile(profile_id) if not profile: emit("rtorrent_error", {"error": "Profile does not exist"}) return diff = torrent_cache.refresh(profile) rows = torrent_cache.snapshot(profile_id) emit("torrent_snapshot", {"profile_id": profile_id, "torrents": rows, "summary": cached_summary(profile_id, rows, force=True), "error": diff.get("error", "")})