"""Haupt-Server: HTTP + WebSocket + Templates in einer aiohttp-App""" import asyncio import logging from pathlib import Path from aiohttp import web import aiohttp_jinja2 import jinja2 from app.config import Config from app.services.queue import QueueService from app.services.scanner import ScannerService from app.services.encoder import EncoderService from app.routes.ws import WebSocketManager from app.services.library import LibraryService from app.services.tvdb import TVDBService from app.services.cleaner import CleanerService from app.services.importer import ImporterService from app.routes.api import setup_api_routes from app.routes.library_api import setup_library_routes from app.routes.pages import setup_page_routes class VideoKonverterServer: """Haupt-Server - ein Port fuer HTTP, WebSocket und Admin-UI""" def __init__(self): self.config = Config() self.config.setup_logging() # Services self.ws_manager = WebSocketManager() self.scanner = ScannerService(self.config) self.queue_service = QueueService(self.config, self.ws_manager) self.ws_manager.set_queue_service(self.queue_service) # Bibliothek-Services self.library_service = LibraryService(self.config, self.ws_manager) self.tvdb_service = TVDBService(self.config) self.cleaner_service = CleanerService(self.config, self.library_service) self.importer_service = ImporterService( self.config, self.library_service, self.tvdb_service ) # aiohttp App (50 GiB Upload-Limit fuer grosse Videodateien) self.app = web.Application( client_max_size=50 * 1024 * 1024 * 1024, middlewares=[self._no_cache_middleware] ) self._setup_app() @web.middleware async def _no_cache_middleware(self, request: web.Request, handler) -> web.Response: """Verhindert Browser-Caching fuer API-Responses""" response = await handler(request) if request.path.startswith("/api/"): response.headers["Cache-Control"] = "no-store, no-cache, must-revalidate" response.headers["Pragma"] = "no-cache" response.headers["Expires"] = "0" return response def _setup_app(self) -> None: """Konfiguriert die aiohttp-Application""" # Jinja2 Templates (request_processor macht request in Templates verfuegbar) template_dir = Path(__file__).parent / "templates" aiohttp_jinja2.setup( self.app, loader=jinja2.FileSystemLoader(str(template_dir)), context_processors=[aiohttp_jinja2.request_processor], ) # WebSocket Route ws_path = self.config.server_config.get("websocket_path", "/ws") self.app.router.add_get(ws_path, self.ws_manager.handle_websocket) # API Routes setup_api_routes( self.app, self.config, self.queue_service, self.scanner, self.ws_manager ) # Bibliothek API Routes setup_library_routes( self.app, self.config, self.library_service, self.tvdb_service, self.queue_service, self.cleaner_service, self.importer_service, ) # Seiten Routes setup_page_routes(self.app, self.config, self.queue_service) # Statische Dateien static_dir = Path(__file__).parent / "static" if static_dir.exists(): self.app.router.add_static( "/static/", path=str(static_dir), name="static" ) # Startup/Shutdown Hooks self.app.on_startup.append(self._on_startup) self.app.on_shutdown.append(self._on_shutdown) async def _on_startup(self, app: web.Application) -> None: """Server-Start: GPU pruefen, Queue starten""" mode = self.config.encoding_mode # Auto-Detection if mode == "auto": gpu_ok = EncoderService.detect_gpu_available() if gpu_ok: gpu_ok = await EncoderService.test_gpu_encoding( self.config.gpu_device ) if gpu_ok: self.config.settings["encoding"]["mode"] = "gpu" self.config.settings["encoding"]["default_preset"] = "gpu_av1" logging.info(f"GPU erkannt ({self.config.gpu_device}), " f"verwende GPU-Encoding") else: self.config.settings["encoding"]["mode"] = "cpu" self.config.settings["encoding"]["default_preset"] = "cpu_av1" logging.info("Keine GPU erkannt, verwende CPU-Encoding") else: logging.info(f"Encoding-Modus: {mode}") # Queue starten await self.queue_service.start() # Bibliothek starten await self.library_service.start() # DB-Pool mit anderen Services teilen if self.library_service._db_pool: self.tvdb_service.set_db_pool(self.library_service._db_pool) self.importer_service.set_db_pool(self.library_service._db_pool) # WebSocket-Manager an ImporterService fuer Live-Updates self.importer_service.set_ws_manager(self.ws_manager) # Zusaetzliche DB-Tabellen erstellen await self.tvdb_service.init_db() await self.importer_service.init_db() host = self.config.server_config.get("host", "0.0.0.0") port = self.config.server_config.get("port", 8080) logging.info(f"Server bereit auf http://{host}:{port}") async def _on_shutdown(self, app: web.Application) -> None: """Server-Stop: Queue und Library stoppen""" await self.queue_service.stop() await self.library_service.stop() logging.info("Server heruntergefahren") async def run(self) -> None: """Startet den Server""" host = self.config.server_config.get("host", "0.0.0.0") port = self.config.server_config.get("port", 8080) runner = web.AppRunner(self.app) await runner.setup() site = web.TCPSite(runner, host, port) await site.start() logging.info( f"VideoKonverter Server laeuft auf http://{host}:{port}\n" f" Dashboard: http://{host}:{port}/\n" f" Bibliothek: http://{host}:{port}/library\n" f" Admin: http://{host}:{port}/admin\n" f" Statistik: http://{host}:{port}/statistics\n" f" WebSocket: ws://{host}:{port}/ws\n" f" API: http://{host}:{port}/api/convert (POST)" ) # Endlos laufen bis Interrupt await asyncio.Event().wait()