"""Proxy manager — background generation and lifecycle of scrub proxies.""" import logging from pathlib import Path from threading import Thread from gi.repository import GLib from cht.scrub.proxy import proxy_path_for, generate_proxy, cleanup_proxies, PROXY_HEIGHT log = logging.getLogger(__name__) class ProxyManager: """Manages background proxy generation for scrub mode. Usage: pm = ProxyManager(session_id="20260403_120000") pm.request(segment_path, on_ready=lambda path: ...) pm.cancel() # stop pending work """ # Proxy states PENDING = "pending" GENERATING = "generating" READY = "ready" FAILED = "failed" def __init__(self, session_id: str): self._session_id = session_id self._state: dict[str, str] = {} # segment_path_str → state self._proxies: dict[str, Path] = {} # segment_path_str → proxy_path self._cancelled = False def request(self, segment_path: Path, on_ready=None, on_error=None) -> None: """Request proxy for a segment. Calls back on GTK main thread when ready. If proxy already exists, calls back immediately. """ key = str(segment_path) # Already ready proxy = proxy_path_for(segment_path, self._session_id) if proxy.exists(): self._state[key] = self.READY self._proxies[key] = proxy if on_ready: GLib.idle_add(on_ready, proxy) return # Already generating if self._state.get(key) == self.GENERATING: return self._state[key] = self.GENERATING def _generate(): if self._cancelled: return try: result = generate_proxy(segment_path, proxy) self._state[key] = self.READY self._proxies[key] = result if on_ready and not self._cancelled: GLib.idle_add(on_ready, result) except Exception as e: self._state[key] = self.FAILED log.error("Proxy generation failed: %s", e) if on_error and not self._cancelled: GLib.idle_add(on_error, str(e)) Thread(target=_generate, daemon=True, name=f"proxy_{segment_path.stem}").start() def get_state(self, segment_path: Path) -> str | None: """Return current state of proxy for segment, or None if not requested.""" return self._state.get(str(segment_path)) def get_proxy(self, segment_path: Path) -> Path | None: """Return proxy path if ready, None otherwise.""" return self._proxies.get(str(segment_path)) def cancel(self) -> None: """Cancel pending work. Already-running ffmpeg will finish but callbacks are suppressed.""" self._cancelled = True def cleanup(self) -> None: """Delete all proxies for this session.""" self.cancel() cleanup_proxies(self._session_id) self._state.clear() self._proxies.clear()