feat: Download-Fortschritt live anzeigen mit Progressbar
This commit is contained in:
parent
f7747688cc
commit
4c51dbcae7
1 changed files with 164 additions and 35 deletions
|
|
@ -5,9 +5,12 @@ Erreichbar via Tailscale: http://100.123.47.7:8765
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import re as _re
|
||||||
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import json
|
import json
|
||||||
import threading
|
import threading
|
||||||
|
import urllib.request
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
@ -38,6 +41,35 @@ def _save_download_log(log):
|
||||||
pass
|
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"""<!DOCTYPE html>
|
HTML = r"""<!DOCTYPE html>
|
||||||
<html lang="de">
|
<html lang="de">
|
||||||
<head>
|
<head>
|
||||||
|
|
@ -295,6 +327,24 @@ HTML = r"""<!DOCTYPE html>
|
||||||
.search-input:focus { border-color: var(--accent); }
|
.search-input:focus { border-color: var(--accent); }
|
||||||
.dl-status { font-size: 15px; color: #ffa726; margin-top: 6px; }
|
.dl-status { font-size: 15px; color: #ffa726; margin-top: 6px; }
|
||||||
.dl-done { color: var(--kino); }
|
.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); }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -361,39 +411,42 @@ let currentFilter = 'all';
|
||||||
let filmInfoCache = {};
|
let filmInfoCache = {};
|
||||||
let infoQueue = [];
|
let infoQueue = [];
|
||||||
let infoLoading = false;
|
let infoLoading = false;
|
||||||
|
let dlProgress = {};
|
||||||
|
let polling = false;
|
||||||
|
|
||||||
function queueInfoLoad(films) {
|
async function loadFilmInfo(films) {
|
||||||
films.forEach(f => {
|
if (infoLoading) return;
|
||||||
if (!filmInfoCache[f.tid] && !infoQueue.some(q => q.tid === f.tid)) {
|
var missing = films.filter(function(f) { return !filmInfoCache[f.tid]; });
|
||||||
infoQueue.push({tid: f.tid, title: f.title});
|
if (!missing.length) return;
|
||||||
}
|
|
||||||
});
|
|
||||||
if (!infoLoading) processInfoQueue();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function processInfoQueue() {
|
|
||||||
if (!infoQueue.length) { infoLoading = false; return; }
|
|
||||||
infoLoading = true;
|
infoLoading = true;
|
||||||
const item = infoQueue.shift();
|
var titles = missing.map(function(f) { return f.title; });
|
||||||
try {
|
try {
|
||||||
const r = await fetch('/api/filminfo?title=' + encodeURIComponent(item.title));
|
var r = await fetch('/api/filminfo_batch', {
|
||||||
const d = await r.json();
|
method: 'POST',
|
||||||
let parts = [];
|
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.year) parts.push(d.year);
|
||||||
if (d.countries && d.countries.length) parts.push(d.countries.join('/'));
|
if (d.countries && d.countries.length) parts.push(d.countries.join('/'));
|
||||||
if (d.genres && d.genres.length) parts.push(d.genres.join(', '));
|
if (d.genres && d.genres.length) parts.push(d.genres.join(', '));
|
||||||
if (parts.length) {
|
if (parts.length) {
|
||||||
filmInfoCache[item.tid] = parts.join(' \u00b7 ');
|
filmInfoCache[f.tid] = parts.join(' \u00b7 ');
|
||||||
const el = document.getElementById('info-' + item.tid);
|
var el = document.getElementById('info-' + f.tid);
|
||||||
if (el) { el.textContent = filmInfoCache[item.tid]; el.classList.add('loaded'); }
|
if (el) { el.textContent = filmInfoCache[f.tid]; el.classList.add('loaded'); }
|
||||||
}
|
}
|
||||||
|
});
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
setTimeout(processInfoQueue, 200);
|
infoLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadFilms() {
|
async function loadFilms() {
|
||||||
const resp = await fetch('/api/films');
|
var resp = await fetch('/api/films');
|
||||||
const data = await resp.json();
|
var data = await resp.json();
|
||||||
allFilms = data.films;
|
allFilms = data.films;
|
||||||
downloads = data.downloads || {};
|
downloads = data.downloads || {};
|
||||||
document.getElementById('loading').style.display = 'none';
|
document.getElementById('loading').style.display = 'none';
|
||||||
|
|
@ -401,6 +454,8 @@ async function loadFilms() {
|
||||||
document.getElementById('header-stats').textContent =
|
document.getElementById('header-stats').textContent =
|
||||||
data.total + ' Aufnahmen \u00b7 ' + data.kino + ' Kino \u00b7 ' + data.urgent + ' dringend';
|
data.total + ' Aufnahmen \u00b7 ' + data.kino + ' Kino \u00b7 ' + data.urgent + ' dringend';
|
||||||
renderFilms();
|
renderFilms();
|
||||||
|
var hasRunning = Object.values(downloads).some(function(v) { return v === 'running'; });
|
||||||
|
if (hasRunning) startPolling();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setFilter(f) {
|
function setFilter(f) {
|
||||||
|
|
@ -457,8 +512,18 @@ function filmCard(f) {
|
||||||
var statusEl = '<div class="checkbox-indicator">' + (sel ? '\u2713' : '') + '</div>';
|
var statusEl = '<div class="checkbox-indicator">' + (sel ? '\u2713' : '') + '</div>';
|
||||||
var extraLine = '';
|
var extraLine = '';
|
||||||
if (dlState === 'running') {
|
if (dlState === 'running') {
|
||||||
|
var prog = dlProgress[String(f.tid)];
|
||||||
statusEl = '<div class="status-icon">\u2b07</div>';
|
statusEl = '<div class="status-icon">\u2b07</div>';
|
||||||
extraLine = '<div class="dl-status">Download l\u00e4uft...</div>';
|
if (prog && prog.expected_mb > 0) {
|
||||||
|
extraLine = '<div class="dl-progress">'
|
||||||
|
+ '<div class="progress-bar"><div class="progress-fill" style="width:' + prog.percent + '%"></div></div>'
|
||||||
|
+ '<span class="progress-text">' + prog.current_mb + ' / ' + prog.expected_mb + ' MB (' + prog.percent + '%)</span>'
|
||||||
|
+ '</div>';
|
||||||
|
} else if (prog) {
|
||||||
|
extraLine = '<div class="dl-status">' + prog.current_mb + ' MB heruntergeladen...</div>';
|
||||||
|
} else {
|
||||||
|
extraLine = '<div class="dl-status">Download startet...</div>';
|
||||||
|
}
|
||||||
} else if (dlState === 'done') {
|
} else if (dlState === 'done') {
|
||||||
statusEl = '<div class="status-icon">\u2705</div>';
|
statusEl = '<div class="status-icon">\u2705</div>';
|
||||||
extraLine = '<div class="dl-status dl-done">Gespeichert</div>';
|
extraLine = '<div class="dl-status dl-done">Gespeichert</div>';
|
||||||
|
|
@ -521,12 +586,46 @@ async function startDownloads() {
|
||||||
body: JSON.stringify({tids: tids})
|
body: JSON.stringify({tids: tids})
|
||||||
});
|
});
|
||||||
var result = await resp.json();
|
var result = await resp.json();
|
||||||
|
var errors = 0;
|
||||||
result.results.forEach(function(r) {
|
result.results.forEach(function(r) {
|
||||||
downloads[r.tid] = r.ok ? 'done' : 'error';
|
if (!r.ok) { downloads[r.tid] = 'error'; errors++; }
|
||||||
});
|
});
|
||||||
renderFilms();
|
renderFilms();
|
||||||
var ok = result.results.filter(function(r) { return r.ok; }).length;
|
var started = result.results.length - errors;
|
||||||
showToast('\u2705 ' + ok + '/' + tids.length + ' Downloads gestartet');
|
showToast('\u2b07 ' + started + ' Download(s) gestartet');
|
||||||
|
if (started > 0) startPolling();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pollProgress() {
|
||||||
|
try {
|
||||||
|
var r = await fetch('/api/download_progress');
|
||||||
|
var data = await r.json();
|
||||||
|
var anyActive = false;
|
||||||
|
for (var tid in data) {
|
||||||
|
var info = data[tid];
|
||||||
|
dlProgress[tid] = info;
|
||||||
|
if (info.done) {
|
||||||
|
downloads[parseInt(tid)] = 'done';
|
||||||
|
delete dlProgress[tid];
|
||||||
|
} else {
|
||||||
|
anyActive = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderFilms();
|
||||||
|
if (anyActive) {
|
||||||
|
setTimeout(pollProgress, 2000);
|
||||||
|
} else {
|
||||||
|
polling = false;
|
||||||
|
}
|
||||||
|
} catch(e) {
|
||||||
|
setTimeout(pollProgress, 3000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function startPolling() {
|
||||||
|
if (polling) return;
|
||||||
|
polling = true;
|
||||||
|
setTimeout(pollProgress, 1500);
|
||||||
}
|
}
|
||||||
|
|
||||||
function showToast(msg) {
|
function showToast(msg) {
|
||||||
|
|
@ -596,6 +695,7 @@ def api_download():
|
||||||
data = request.get_json()
|
data = request.get_json()
|
||||||
tids = data.get("tids", [])
|
tids = data.get("tids", [])
|
||||||
dl_log = _load_download_log()
|
dl_log = _load_download_log()
|
||||||
|
progress = _load_progress()
|
||||||
results = []
|
results = []
|
||||||
|
|
||||||
films_by_tid = {}
|
films_by_tid = {}
|
||||||
|
|
@ -608,11 +708,40 @@ def api_download():
|
||||||
|
|
||||||
def download_one(tid):
|
def download_one(tid):
|
||||||
title = films_by_tid.get(tid, f"film_{tid}")
|
title = films_by_tid.get(tid, f"film_{tid}")
|
||||||
filename, err = savetv._download_film(tid, title)
|
url, err = savetv._get_download_url(tid)
|
||||||
if err:
|
if err:
|
||||||
app.logger.error("Download TID %s: %s", tid, err)
|
try:
|
||||||
return {"tid": tid, "ok": False, "error": err}
|
url, err = savetv._get_download_url(tid, fmt=savetv.DOWNLOAD_FORMAT_SD)
|
||||||
dl_log[str(tid)] = "done"
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
if err:
|
||||||
|
return {"tid": tid, "ok": False, "error": f"URL-Fehler: {err}"}
|
||||||
|
|
||||||
|
expected_bytes = _head_content_length(url)
|
||||||
|
safe_title = _re.sub(r'[^\w\-.]', '_', title)[:80]
|
||||||
|
filename = f"{safe_title}_{tid}.mp4"
|
||||||
|
target = DOWNLOAD_DIR / filename
|
||||||
|
|
||||||
|
progress[str(tid)] = {
|
||||||
|
"filename": filename,
|
||||||
|
"expected_bytes": expected_bytes,
|
||||||
|
"started_at": datetime.now().isoformat(),
|
||||||
|
}
|
||||||
|
_save_progress(progress)
|
||||||
|
|
||||||
|
DOWNLOAD_DIR.mkdir(parents=True, exist_ok=True)
|
||||||
|
try:
|
||||||
|
subprocess.Popen(
|
||||||
|
["wget", "-q", "-O", str(target), url],
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
progress.pop(str(tid), None)
|
||||||
|
_save_progress(progress)
|
||||||
|
return {"tid": tid, "ok": False, "error": f"wget: {e}"}
|
||||||
|
|
||||||
|
dl_log[str(tid)] = "running"
|
||||||
return {"tid": tid, "ok": True, "filename": filename}
|
return {"tid": tid, "ok": True, "filename": filename}
|
||||||
|
|
||||||
threads = []
|
threads = []
|
||||||
|
|
@ -628,7 +757,7 @@ def api_download():
|
||||||
threads.append(t)
|
threads.append(t)
|
||||||
|
|
||||||
for t in threads:
|
for t in threads:
|
||||||
t.join(timeout=15)
|
t.join(timeout=30)
|
||||||
|
|
||||||
results = [r for r in result_list if r]
|
results = [r for r in result_list if r]
|
||||||
_save_download_log(dl_log)
|
_save_download_log(dl_log)
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue