docker.videokonverter/video-konverter/app/server.py
data 99730f2f8f feat: VideoKonverter v3.1 - TV-App, Auth, Tizen, Log-API
TV-App (/tv/):
- Login mit bcrypt-Passwort-Hashing und DB-Sessions (30 Tage)
- Home (Weiterschauen, Serien, Filme), Serien-Detail mit Staffeln
- Film-Uebersicht und Detail, Fullscreen Video-Player
- Suche mit Live-Ergebnissen, Watch-Progress (alle 10s gespeichert)
- D-Pad/Fernbedienung-Navigation (FocusManager, Samsung Tizen Keys)
- PWA: manifest.json, Service Worker, Icons fuer Handy/Tablet
- Pro-User Berechtigungen (Serien, Filme, Admin, erlaubte Pfade)

Admin-Erweiterungen:
- QR-Code fuer TV-App URL
- User-Verwaltung (CRUD) mit Rechte-Konfiguration
- Log-API: GET /api/log?lines=100&level=INFO

Tizen-App (tizen-app/):
- Wrapper-App fuer Samsung Smart TVs (.wgt Paket)
- Einmalige Server-IP Eingabe, danach automatische Verbindung
- Installationsanleitung (INSTALL.md)

Bug-Fixes:
- executeImport: Job-ID vor resetImport() gesichert
- cursor(aiomysql.DictCursor) statt cursor(dict)
- DB-Spalten width/height statt video_width/video_height

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 09:26:19 +01:00

191 lines
7.2 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.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"""
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)
# TV-App Routes (Auth-Service wird spaeter mit DB-Pool initialisiert)
self.auth_service = None
# 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 initialisieren (braucht DB-Pool)
if self.library_service._db_pool:
async def _get_pool():
return self.library_service._db_pool
self.auth_service = AuthService(_get_pool)
await self.auth_service.init_db()
setup_tv_routes(
self.app, self.config,
self.auth_service, self.library_service,
)
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()