diff --git a/homelab-ai-bot/savetv_web.py b/homelab-ai-bot/savetv_web.py index f8f3ad80..ec65e19e 100644 --- a/homelab-ai-bot/savetv_web.py +++ b/homelab-ai-bot/savetv_web.py @@ -5,9 +5,12 @@ Erreichbar via Tailscale: http://100.123.47.7:8765 """ import os +import re as _re +import subprocess import sys import json import threading +import urllib.request from datetime import datetime from pathlib import Path @@ -38,6 +41,35 @@ def _save_download_log(log): pass +DOWNLOAD_PROGRESS = Path("/mnt/savetv/.download_progress.json") +DOWNLOAD_DIR = Path("/mnt/savetv") + + +def _load_progress(): + if DOWNLOAD_PROGRESS.exists(): + try: + return json.loads(DOWNLOAD_PROGRESS.read_text()) + except Exception: + pass + return {} + + +def _save_progress(prog): + try: + DOWNLOAD_PROGRESS.write_text(json.dumps(prog, ensure_ascii=False, indent=2)) + except Exception: + pass + + +def _head_content_length(url): + try: + req = urllib.request.Request(url, method='HEAD') + with urllib.request.urlopen(req, timeout=10) as resp: + return int(resp.headers.get('Content-Length', 0)) + except Exception: + return 0 + + HTML = r"""
@@ -295,6 +327,24 @@ HTML = r""" .search-input:focus { border-color: var(--accent); } .dl-status { font-size: 15px; color: #ffa726; margin-top: 6px; } .dl-done { color: var(--kino); } + .dl-progress { margin-top: 10px; } + .progress-bar { + background: var(--border); + border-radius: 4px; + height: 8px; + overflow: hidden; + margin-bottom: 6px; + } + .progress-fill { + background: linear-gradient(90deg, #ffa726, #ff8f00); + height: 100%; + border-radius: 4px; + transition: width .6s ease; + min-width: 2px; + } + .progress-fill.done { background: linear-gradient(90deg, var(--kino), #2e9e6a); } + .progress-text { font-size: 14px; color: #ffa726; } + .progress-text.done { color: var(--kino); } @@ -361,39 +411,42 @@ let currentFilter = 'all'; let filmInfoCache = {}; let infoQueue = []; let infoLoading = false; +let dlProgress = {}; +let polling = false; -function queueInfoLoad(films) { - films.forEach(f => { - if (!filmInfoCache[f.tid] && !infoQueue.some(q => q.tid === f.tid)) { - infoQueue.push({tid: f.tid, title: f.title}); - } - }); - if (!infoLoading) processInfoQueue(); -} - -async function processInfoQueue() { - if (!infoQueue.length) { infoLoading = false; return; } +async function loadFilmInfo(films) { + if (infoLoading) return; + var missing = films.filter(function(f) { return !filmInfoCache[f.tid]; }); + if (!missing.length) return; infoLoading = true; - const item = infoQueue.shift(); + var titles = missing.map(function(f) { return f.title; }); try { - const r = await fetch('/api/filminfo?title=' + encodeURIComponent(item.title)); - const d = await r.json(); - let parts = []; - if (d.year) parts.push(d.year); - if (d.countries && d.countries.length) parts.push(d.countries.join('/')); - if (d.genres && d.genres.length) parts.push(d.genres.join(', ')); - if (parts.length) { - filmInfoCache[item.tid] = parts.join(' \u00b7 '); - const el = document.getElementById('info-' + item.tid); - if (el) { el.textContent = filmInfoCache[item.tid]; el.classList.add('loaded'); } - } + var r = await fetch('/api/filminfo_batch', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({titles: titles}) + }); + var data = await r.json(); + missing.forEach(function(f) { + var d = data[f.title]; + if (!d) return; + var parts = []; + if (d.year) parts.push(d.year); + if (d.countries && d.countries.length) parts.push(d.countries.join('/')); + if (d.genres && d.genres.length) parts.push(d.genres.join(', ')); + if (parts.length) { + filmInfoCache[f.tid] = parts.join(' \u00b7 '); + var el = document.getElementById('info-' + f.tid); + if (el) { el.textContent = filmInfoCache[f.tid]; el.classList.add('loaded'); } + } + }); } catch(e) {} - setTimeout(processInfoQueue, 200); + infoLoading = false; } async function loadFilms() { - const resp = await fetch('/api/films'); - const data = await resp.json(); + var resp = await fetch('/api/films'); + var data = await resp.json(); allFilms = data.films; downloads = data.downloads || {}; document.getElementById('loading').style.display = 'none'; @@ -401,6 +454,8 @@ async function loadFilms() { document.getElementById('header-stats').textContent = data.total + ' Aufnahmen \u00b7 ' + data.kino + ' Kino \u00b7 ' + data.urgent + ' dringend'; renderFilms(); + var hasRunning = Object.values(downloads).some(function(v) { return v === 'running'; }); + if (hasRunning) startPolling(); } function setFilter(f) { @@ -457,8 +512,18 @@ function filmCard(f) { var statusEl = '