Initial import
This commit is contained in:
@@ -0,0 +1,173 @@
|
||||
"""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])))
|
||||
Reference in New Issue
Block a user