"""Per-browser user sessions and idle cleanup.""" import os import secrets import shutil import threading import time from fastapi import Request from torrent2drive.config import BASE_TEMP from torrent2drive.constants import DEFAULT_ENC class UserSession: """Isolated state for a single browser session / user.""" def __init__(self, sid: str): self.sid = sid self.lock = threading.Lock() self.last_seen = time.time() self.dl_dir = os.path.join(BASE_TEMP, sid, "downloads") os.makedirs(self.dl_dir, exist_ok=True) self.handle = None self.ti = None self.selected_file_indices = [] self.download_started = False self.download_finished = False self.is_transferring = False self.is_completed = False self.transfer_status = "" self.use_ffmpeg = False self.ff_progress = 0.0 self.ff_eta = "" self.ff_speed = "" self.ff_size_done = "" self.ff_file_label = "" self.ff_running = False self.ff_stage = "" self.ff_duration_s = 0.0 self.ff_time_done_s = 0.0 self.pipeline_total = 0 self.pipeline_current = 0 self.pipeline_current_name = "" self.pipeline_failed: list[str] = [] self.pipeline_skipped: list[str] = [] self.pipeline_done: list[str] = [] self.pipeline_overall_pct = 0.0 self.pipeline_error = "" self.enc = dict(DEFAULT_ENC) self.stream_watch_only = False self.stream_watch_active = False self.stream_watch_index = -1 def touch(self): self.last_seen = time.time() _sessions: dict[str, UserSession] = {} _sessions_lock = threading.Lock() def get_or_create_session(request: Request) -> UserSession: sid = request.session.get("sid") if not sid: sid = secrets.token_urlsafe(16) request.session["sid"] = sid with _sessions_lock: us = _sessions.get(sid) if us is None: us = UserSession(sid) _sessions[sid] = us us.touch() return us def get_session_by_sid(sid: str) -> UserSession | None: with _sessions_lock: return _sessions.get(sid) def _gc_sessions(idle_seconds=3600 * 6): now = time.time() drop = [] with _sessions_lock: for sid, us in _sessions.items(): if ( us.is_transferring or us.ff_running or us.download_started or us.stream_watch_active ): continue if now - us.last_seen > idle_seconds: drop.append(sid) for sid in drop: us = _sessions.pop(sid, None) if not us: continue try: from torrent2drive.torrent_engine import remove_handle remove_handle(us.handle) except Exception: pass try: shutil.rmtree(os.path.join(BASE_TEMP, sid), ignore_errors=True) except Exception: pass def _gc_loop(): while True: try: _gc_sessions() except Exception as e: print(f"[GC] {e}") time.sleep(900) def start_gc_loop(): threading.Thread(target=_gc_loop, daemon=True).start()