docker.videokonverter/video-konverter/app/server.py
data c7151e8bd1 feat: VideoKonverter v4.0.1 - UX-Verbesserungen, Batch-Thumbnails, Bugfixes
- Alphabet-Seitenleiste (A-Z) auf Serien-/Filme-Seite
- Separate Player-Buttons fuer Audio/Untertitel/Qualitaet
- Batch-Thumbnail-Generierung per Button in der Bibliothek
- Redundante Dateien in Episoden-Tabelle orange markiert
- Gesehen-Markierung per Episode/Staffel
- Genre-Filter als Select-Element statt Chips
- Fix: tvdb_episode_cache fehlende Spalten (overview, image_url)
- Fix: Login Auto-Fill-Erkennung statt Flash
- Fix: Profil-Wechsel zeigt alle User

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-01 09:22:04 +01:00

206 lines
7.9 KiB
Python

"""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.services.auth import AuthService
from app.services.i18n import load_translations, setup_jinja2_i18n
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
from app.routes.tv_api import setup_tv_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 + Error-Logging"""
try:
response = await handler(request)
except web.HTTPException as he:
if he.status >= 500:
logging.error(f"HTTP {he.status} bei {request.method} {request.path}: {he.reason}",
exc_info=True)
raise
except Exception as e:
logging.error(f"Unbehandelte Ausnahme bei {request.method} {request.path}: {e}",
exc_info=True)
raise
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],
)
# i18n: Uebersetzungen laden und Jinja2-Filter registrieren
static_dir = Path(__file__).parent / "static"
load_translations(str(static_dir))
setup_jinja2_i18n(self.app)
# 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)
# TV-App Routes (Auth-Service, DB-Pool wird in on_startup gesetzt)
async def _lazy_pool():
return self.library_service._db_pool
self.auth_service = AuthService(_lazy_pool)
setup_tv_routes(
self.app, self.config,
self.auth_service, self.library_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()
# TV-App Auth-Service: DB-Tabellen initialisieren (Pool kommt ueber lazy getter)
if self.library_service._db_pool:
await self.auth_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" TV-App: http://{host}:{port}/tv/\n"
f" WebSocket: ws://{host}:{port}/ws\n"
f" API: http://{host}:{port}/api/convert (POST)"
)
# Endlos laufen bis Interrupt
await asyncio.Event().wait()