PROBLEME BEHOBEN: - Schwarzes Bild beim Video-Abspielen (z-index & iframe-Overlap) - Login-Cookie wurde nicht gesetzt (Third-Party-Cookie-Blocking) ÄNDERUNGEN: Tizen-App (tizen-app/index.html): - z-index AVPlay von 0 auf 10 erhöht (über iframe) - iframe wird beim AVPlay-Start ausgeblendet (opacity: 0, pointerEvents: none) - iframe wird beim AVPlay-Stop wieder eingeblendet - Fix: <object id="avplayer"> nur im Parent, NICHT im iframe Player-Template (video-konverter/app/templates/tv/player.html): - <object id="avplayer"> entfernt (existiert nur im Parent-Frame) - AVPlay läuft ausschließlich im Tizen-App Parent-Frame Cookie-Fix (video-konverter/app/routes/tv_api.py): - SameSite=Lax → SameSite=None (4 Stellen) - Ermöglicht Session-Cookies im Cross-Origin-iframe - Login funktioniert jetzt in Tizen-App (tizen:// → http://) Neue Features: - VKNative Bridge (vknative-bridge.js): postMessage-Kommunikation iframe ↔ Parent - AVPlay Bridge (avplay-bridge.js): Legacy Direct-Play Support - Android-App Scaffolding (android-app/) TESTERGEBNIS: - ✅ Login erfolgreich (SameSite=None Cookie) - ✅ AVPlay Direct-Play funktioniert (samsung-agent/1.1) - ✅ Bildqualität gut (Hardware-Decoding) - ✅ Keine Stream-Unterbrechungen - ✅ Watch-Progress-Tracking funktioniert Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
243 lines
9.5 KiB
Python
243 lines
9.5 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"]["force_transcode"] = (
|
|
data.get("force_transcode") == "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",
|
|
)
|
|
|
|
async def htmx_save_preset(request: web.Request) -> web.Response:
|
|
"""POST /htmx/preset/{preset_name} - Preset via Formular speichern"""
|
|
preset_name = request.match_info["preset_name"]
|
|
data = await request.post()
|
|
|
|
# Extra-Params parsen (key=value pro Zeile)
|
|
extra_params = {}
|
|
raw_extra = data.get("extra_params", "").strip()
|
|
for line in raw_extra.splitlines():
|
|
line = line.strip()
|
|
if "=" in line:
|
|
k, v = line.split("=", 1)
|
|
extra_params[k.strip()] = v.strip()
|
|
|
|
# Speed-Preset: int oder string oder None
|
|
speed_raw = data.get("speed_preset", "").strip()
|
|
speed_preset = None
|
|
if speed_raw:
|
|
try:
|
|
speed_preset = int(speed_raw)
|
|
except ValueError:
|
|
speed_preset = speed_raw
|
|
|
|
preset = {
|
|
"name": data.get("name", preset_name),
|
|
"video_codec": data.get("video_codec", "libx264"),
|
|
"container": data.get("container", "mp4"),
|
|
"quality_param": data.get("quality_param", "crf"),
|
|
"quality_value": int(data.get("quality_value", 23)),
|
|
"gop_size": int(data.get("gop_size", 240)),
|
|
"speed_preset": speed_preset,
|
|
"video_filter": data.get("video_filter", ""),
|
|
"hw_init": data.get("hw_init") == "on",
|
|
"extra_params": extra_params,
|
|
}
|
|
|
|
config.presets[preset_name] = preset
|
|
config.save_presets()
|
|
logging.info(f"Preset '{preset_name}' via Admin-UI gespeichert")
|
|
|
|
return web.Response(
|
|
text='<div class="toast success">Preset 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_post("/htmx/preset/{preset_name}", htmx_save_preset)
|
|
app.router.add_get("/htmx/stats", htmx_stats_table)
|