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>
76 lines
2.8 KiB
HTML
76 lines
2.8 KiB
HTML
{% extends "tv/base.html" %}
|
|
{% block title %}{{ series.title or series.folder_name }} - VideoKonverter TV{% endblock %}
|
|
|
|
{% block content %}
|
|
<section class="tv-section">
|
|
<!-- Serien-Header -->
|
|
<div class="tv-detail-header">
|
|
{% if series.poster_url %}
|
|
<img src="{{ series.poster_url }}" alt="" class="tv-detail-poster">
|
|
{% endif %}
|
|
<div class="tv-detail-info">
|
|
<h1 class="tv-page-title">{{ series.title or series.folder_name }}</h1>
|
|
{% if series.genres %}
|
|
<p class="tv-detail-genres">{{ series.genres }}</p>
|
|
{% endif %}
|
|
{% if series.overview %}
|
|
<p class="tv-detail-overview">{{ series.overview }}</p>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Staffel-Tabs -->
|
|
{% if seasons %}
|
|
<div class="tv-tabs" id="season-tabs">
|
|
{% for sn in seasons.keys() %}
|
|
<button class="tv-tab {% if loop.first %}active{% endif %}"
|
|
data-focusable
|
|
onclick="showSeason({{ sn }})">
|
|
{% if sn == 0 %}Specials{% else %}Staffel {{ sn }}{% endif %}
|
|
</button>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<!-- Episoden pro Staffel -->
|
|
{% for sn, episodes in seasons.items() %}
|
|
<div class="tv-season" id="season-{{ sn }}" {% if not loop.first %}style="display:none"{% endif %}>
|
|
<div class="tv-episode-list">
|
|
{% for ep in episodes %}
|
|
<a href="/tv/player?v={{ ep.id }}" class="tv-episode" data-focusable>
|
|
<span class="tv-episode-num">
|
|
{% if ep.episode_number %}E{{ "%02d"|format(ep.episode_number) }}{% else %}-{% endif %}
|
|
</span>
|
|
<span class="tv-episode-title">
|
|
{{ ep.episode_title or ep.file_name }}
|
|
</span>
|
|
<span class="tv-episode-meta">
|
|
{% if ep.duration_sec %}{{ (ep.duration_sec / 60)|round|int }} Min{% endif %}
|
|
{% if ep.width %} · {{ ep.width }}x{{ ep.height }}{% endif %}
|
|
</span>
|
|
<span class="tv-episode-play">▶</span>
|
|
</a>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
{% else %}
|
|
<div class="tv-empty">Keine Episoden vorhanden.</div>
|
|
{% endif %}
|
|
</section>
|
|
{% endblock %}
|
|
|
|
{% block scripts %}
|
|
<script>
|
|
function showSeason(sn) {
|
|
// Alle Staffeln verstecken
|
|
document.querySelectorAll('.tv-season').forEach(el => el.style.display = 'none');
|
|
// Alle Tabs deaktivieren
|
|
document.querySelectorAll('.tv-tab').forEach(el => el.classList.remove('active'));
|
|
// Gewaehlte Staffel anzeigen
|
|
const season = document.getElementById('season-' + sn);
|
|
if (season) season.style.display = '';
|
|
// Tab aktivieren
|
|
event.target.classList.add('active');
|
|
}
|
|
</script>
|
|
{% endblock %}
|