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>
191 lines
7.2 KiB
Python
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()
|