docker.videokonverter/video-konverter/app/routes/pages.py
data 4f151de78c feat: VideoKonverter v4.2 - TV Admin-Center, HLS-Streaming, Startseiten-Rubriken
- TV Admin-Center (/tv-admin): HLS-Settings, Session-Monitoring, User-Verwaltung
- HLS-Streaming: ffmpeg .ts-Segmente, hls.js, GPU VAAPI, SIGSTOP/SIGCONT
- Startseite: Rubriken (Weiterschauen, Neu, Serien, Filme, Schon gesehen)
- User-Settings: Startseiten-Rubriken konfigurierbar, Watch-Threshold
- UI: Amber/Gold Accent-Farbe, Focus-Ring-Fix, Player-Buttons einheitlich
- Cache-Busting: ?v= Timestamp auf allen CSS/JS Includes

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 17:57:48 +01:00

195 lines
7.6 KiB
Python

"""Server-gerenderte Seiten mit Jinja2 + HTMX"""
import logging
from aiohttp import web
import aiohttp_jinja2
from app.config import Config
from app.services.queue import QueueService
from app.services.encoder import EncoderService
from app.models.media import MediaFile
def setup_page_routes(app: web.Application, config: Config,
queue_service: QueueService) -> None:
"""Registriert Seiten-Routes"""
def _build_ws_url(request) -> str:
"""Baut WebSocket-URL fuer den Client"""
srv = config.server_config
ext_url = srv.get("external_url", "")
use_https = srv.get("use_https", False)
ws_path = srv.get("websocket_path", "/ws")
protocol = "wss" if use_https else "ws"
if ext_url:
return f"{protocol}://{ext_url}{ws_path}"
return f"{protocol}://{request.host}{ws_path}"
@aiohttp_jinja2.template("dashboard.html")
async def dashboard(request: web.Request) -> dict:
"""GET / - Dashboard"""
return {
"ws_url": _build_ws_url(request),
"active_jobs": queue_service.get_active_jobs().get("data_convert", {}),
"queue": queue_service.get_queue_state().get("data_queue", {}),
}
@aiohttp_jinja2.template("admin.html")
async def admin(request: web.Request) -> dict:
"""GET /admin - Einstellungsseite"""
gpu_available = EncoderService.detect_gpu_available()
gpu_devices = EncoderService.get_available_render_devices()
return {
"settings": config.settings,
"presets": config.presets,
"gpu_available": gpu_available,
"gpu_devices": gpu_devices,
}
@aiohttp_jinja2.template("tv_admin.html")
async def tv_admin(request: web.Request) -> dict:
"""GET /tv-admin - TV Admin-Center"""
tv = config.settings.get("tv", {})
return {"tv": tv}
@aiohttp_jinja2.template("library.html")
async def library(request: web.Request) -> dict:
"""GET /library - Bibliothek"""
return {}
@aiohttp_jinja2.template("statistics.html")
async def statistics(request: web.Request) -> dict:
"""GET /statistics - Statistik-Seite"""
entries = await queue_service.get_statistics(limit=50)
summary = await queue_service.get_statistics_summary()
return {
"entries": entries,
"summary": summary,
"format_size": MediaFile.format_size,
"format_time": MediaFile.format_time,
}
# --- HTMX Partials ---
async def htmx_save_settings(request: web.Request) -> web.Response:
"""POST /htmx/settings - Settings via Formular speichern"""
data = await request.post()
# Formular-Daten in Settings-Struktur konvertieren
settings = config.settings
# Encoding
settings["encoding"]["mode"] = data.get("encoding_mode", "cpu")
settings["encoding"]["gpu_device"] = data.get("gpu_device",
"/dev/dri/renderD128")
settings["encoding"]["default_preset"] = data.get("default_preset",
"cpu_av1")
settings["encoding"]["max_parallel_jobs"] = int(
data.get("max_parallel_jobs", 1)
)
# Files
settings["files"]["target_container"] = data.get("target_container", "webm")
settings["files"]["target_folder"] = data.get("target_folder", "same")
settings["files"]["delete_source"] = data.get("delete_source") == "on"
settings["files"]["recursive_scan"] = data.get("recursive_scan") == "on"
# Cleanup
settings["cleanup"]["enabled"] = data.get("cleanup_enabled") == "on"
cleanup_ext = data.get("cleanup_extensions", "")
if cleanup_ext:
settings["cleanup"]["delete_extensions"] = [
e.strip() for e in cleanup_ext.split(",") if e.strip()
]
exclude_pat = data.get("cleanup_exclude", "")
if exclude_pat:
settings["cleanup"]["exclude_patterns"] = [
p.strip() for p in exclude_pat.split(",") if p.strip()
]
# Audio
audio_langs = data.get("audio_languages", "ger,eng,und")
settings["audio"]["languages"] = [
l.strip() for l in audio_langs.split(",") if l.strip()
]
settings["audio"]["default_codec"] = data.get("audio_codec", "libopus")
settings["audio"]["keep_channels"] = data.get("keep_channels") == "on"
# Subtitle
sub_langs = data.get("subtitle_languages", "ger,eng")
settings["subtitle"]["languages"] = [
l.strip() for l in sub_langs.split(",") if l.strip()
]
# Logging
settings["logging"]["level"] = data.get("log_level", "INFO")
# Bibliothek / TVDB
settings.setdefault("library", {})
settings["library"]["tvdb_api_key"] = data.get("tvdb_api_key", "")
settings["library"]["tvdb_pin"] = data.get("tvdb_pin", "")
settings["library"]["tvdb_language"] = data.get("tvdb_language", "deu")
config.save_settings()
logging.info("Settings via Admin-UI gespeichert")
# Erfolgs-HTML zurueckgeben (HTMX swap)
return web.Response(
text='<div class="toast success">Settings gespeichert!</div>',
content_type="text/html",
)
async def htmx_save_tv_settings(request: web.Request) -> web.Response:
"""POST /htmx/tv-settings - TV-Settings via Formular speichern"""
data = await request.post()
settings = config.settings
settings.setdefault("tv", {})
settings["tv"]["hls_segment_duration"] = int(
data.get("hls_segment_duration", 4))
settings["tv"]["hls_init_duration"] = int(
data.get("hls_init_duration", 1))
settings["tv"]["hls_session_timeout_min"] = int(
data.get("hls_session_timeout_min", 5))
settings["tv"]["hls_max_sessions"] = int(
data.get("hls_max_sessions", 5))
settings["tv"]["pause_batch_on_stream"] = (
data.get("pause_batch_on_stream") == "on")
settings["tv"]["watched_threshold_pct"] = int(
data.get("watched_threshold_pct", 90))
config.save_settings()
logging.info("TV-Settings via Admin-UI gespeichert")
return web.Response(
text='<div class="toast success">TV-Settings gespeichert!</div>',
content_type="text/html",
)
@aiohttp_jinja2.template("partials/stats_table.html")
async def htmx_stats_table(request: web.Request) -> dict:
"""GET /htmx/stats?page=1 - Paginierte Statistik"""
page = int(request.query.get("page", 1))
limit = 25
offset = (page - 1) * limit
entries = await queue_service.get_statistics(limit, offset)
return {
"entries": entries,
"page": page,
"format_size": MediaFile.format_size,
"format_time": MediaFile.format_time,
}
async def redirect_to_library(request: web.Request):
"""GET / -> Weiterleitung zur Bibliothek"""
raise web.HTTPFound("/library")
# Routes registrieren
app.router.add_get("/", redirect_to_library)
app.router.add_get("/dashboard", dashboard)
app.router.add_get("/library", library)
app.router.add_get("/admin", admin)
app.router.add_get("/tv-admin", tv_admin)
app.router.add_get("/statistics", statistics)
app.router.add_post("/htmx/settings", htmx_save_settings)
app.router.add_post("/htmx/tv-settings", htmx_save_tv_settings)
app.router.add_get("/htmx/stats", htmx_stats_table)