"""Application lifecycle – wires together UI, DLNA networking and playback.""" from __future__ import annotations import asyncio import logging import queue import threading from typing import Any from r36s_dlna_browser.dlna.browser_state import BrowserState from r36s_dlna_browser.dlna.discovery import DLNADiscovery from r36s_dlna_browser.dlna.client import DLNAClient from r36s_dlna_browser.player.gstreamer_backend import GStreamerBackend from r36s_dlna_browser.platform.runtime import configure_logging, sdl_env_setup from r36s_dlna_browser.ui.sdl_app import SDLApp log = logging.getLogger(__name__) class Application: """Top-level application that owns the event loop, UI and networking.""" def __init__(self) -> None: configure_logging() sdl_env_setup() self.ui_queue: queue.Queue[Any] = queue.Queue() self.cmd_queue: queue.Queue[Any] = queue.Queue() self.browser_state = BrowserState() self.discovery = DLNADiscovery() self.client = DLNAClient() self.player = GStreamerBackend() self.player.set_event_callback(self._handle_player_event) self._async_loop: asyncio.AbstractEventLoop | None = None self._net_thread: threading.Thread | None = None self._shutting_down = False # ── public API ─────────────────────────────────────────────── def run(self) -> None: """Start the networking thread and enter the SDL2 main loop.""" self._start_network_thread() self.browser_state.loading = True self._schedule(("discover",)) sdl = SDLApp( ui_queue=self.ui_queue, cmd_queue=self.cmd_queue, browser_state=self.browser_state, player=self.player, ) sdl.run() # blocks until quit def shutdown(self) -> None: self._shutting_down = True self._schedule(("shutdown",)) if self._net_thread and self._net_thread.is_alive(): self._net_thread.join(timeout=2) self.player.shutdown() log.info("Application shut down.") # ── networking thread ──────────────────────────────────────── def _start_network_thread(self) -> None: self._net_thread = threading.Thread( target=self._run_async_loop, daemon=True, name="dlna-net" ) self._net_thread.start() def _run_async_loop(self) -> None: self._async_loop = asyncio.new_event_loop() asyncio.set_event_loop(self._async_loop) try: self._async_loop.run_until_complete(self._command_dispatcher()) finally: pending = [ task for task in asyncio.all_tasks(self._async_loop) if not task.done() ] for task in pending: task.cancel() if pending: self._async_loop.run_until_complete( asyncio.gather(*pending, return_exceptions=True) ) self._async_loop.close() async def _command_dispatcher(self) -> None: """Poll cmd_queue for commands from the UI thread and dispatch them.""" loop = asyncio.get_running_loop() while not self._shutting_down: try: cmd = await loop.run_in_executor(None, self.cmd_queue.get, True, 0.1) except queue.Empty: continue try: await self._handle_command(cmd) except Exception as exc: log.exception("Error handling command %s", cmd) self.browser_state.loading = False self.ui_queue.put(("error", str(exc))) async def _handle_command(self, cmd: tuple) -> None: action = cmd[0] if action == "discover": servers = await self.discovery.find_servers() self.browser_state.set_servers(servers) self.browser_state.loading = False self.ui_queue.put(("servers_updated",)) elif action == "browse": server_location = cmd[1] object_id = cmd[2] items = await self.client.browse(server_location, object_id) self.browser_state.set_items(items, object_id) self.browser_state.loading = False self.ui_queue.put(("items_updated",)) elif action == "play": url = cmd[1] title = cmd[2] if len(cmd) > 2 else "" self.player.play(url) self.browser_state.playback_paused = False self.ui_queue.put(("playback_started", title)) elif action == "stop": self.player.stop() elif action == "toggle_pause": paused = self.player.toggle_pause() self.browser_state.playback_paused = paused self.ui_queue.put(("playback_paused" if paused else "playback_resumed",)) elif action == "seek": self.player.seek(cmd[1]) elif action == "volume": volume = self.player.change_volume(cmd[1]) self.browser_state.playback_volume = volume elif action == "shutdown": self._shutting_down = True self.player.shutdown() # ── helpers ────────────────────────────────────────────────── def _schedule(self, cmd: tuple) -> None: self.cmd_queue.put(cmd) def _handle_player_event(self, event: str, *args: Any) -> None: if event == "stopped": self.browser_state.playback_paused = False self.ui_queue.put(("playback_stopped",)) elif event == "position" and args: self.browser_state.playback_position = float(args[0]) elif event == "duration" and args: self.browser_state.playback_duration = float(args[0]) elif event == "paused" and args: self.browser_state.playback_paused = bool(args[0]) elif event == "volume" and args: self.browser_state.playback_volume = int(args[0]) elif event == "buffering" and args: self.browser_state.playback_buffer_percent = int(args[0]) elif event == "cache" and args: self.browser_state.playback_cache_seconds = float(args[0]) elif event == "resolution" and args: self.browser_state.playback_resolution = str(args[0]) elif event == "error" and args: self.browser_state.playback_paused = False self.ui_queue.put(("error", str(args[0])))