diff --git a/homelab-ai-bot/savetv_extra_routes.py b/homelab-ai-bot/savetv_extra_routes.py index 62b97b58..c3f89541 100644 --- a/homelab-ai-bot/savetv_extra_routes.py +++ b/homelab-ai-bot/savetv_extra_routes.py @@ -165,98 +165,226 @@ def register_extra_routes(app, progress_lock=None, load_progress_raw=None, save_ 500, ) + NAS_DONE_FILE = SAVETV_DIR / ".nas_done.json" + NAS_SYNC_MIN_H = 12.0 + NAS_SYNC_JITTER_H = 0.5 # 0-30 min + + def _load_nas_done(): + try: + if NAS_DONE_FILE.exists(): + return _json.loads(NAS_DONE_FILE.read_text()) + except Exception: + pass + return {} + + def _save_nas_done(d): + try: + NAS_DONE_FILE.write_text(_json.dumps(d, ensure_ascii=False, indent=2)) + except Exception: + pass + + @app.route("/api/nas_synced", methods=["POST"]) + def api_nas_synced(): + data = request.get_json(silent=True) or {} + name = data.get("name", "").strip() + if not name: + return jsonify({"ok": False}), 400 + done = _load_nas_done() + from datetime import datetime as _dt + done[name] = _dt.now().strftime("%Y-%m-%dT%H:%M:%S") + _save_nas_done(done) + return jsonify({"ok": True}) + + @app.route("/api/pipeline") + def api_pipeline(): + import math as _math + now = _time.time() + done = _load_nas_done() + hetzner, pending, transferred = [], [], [] + try: + for fp in SAVETV_DIR.iterdir(): + if fp.suffix != ".mp4": + continue + st = fp.stat() + if st.st_size < 1_000_000: + continue + age_h = (now - st.st_mtime) / 3600 + size_mb = round(st.st_size / 1024 / 1024, 1) + name = fp.name + clean = name.rsplit(".", 1)[0] + if name in done: + continue + eta_h = max(0, NAS_SYNC_MIN_H - age_h) + entry = {"name": name, "clean": clean, + "size_mb": size_mb, "age_h": round(age_h, 2), + "mtime": int(st.st_mtime)} + if eta_h > 0: + entry["eta_h"] = round(eta_h, 2) + pending.append(entry) + else: + hetzner.append(entry) + except Exception as e: + pass + hetzner.sort(key=lambda x: x["mtime"], reverse=True) + pending.sort(key=lambda x: x.get("eta_h", 0)) + from datetime import datetime as _dt + for name, ts in sorted(done.items(), key=lambda x: x[1], reverse=True)[:20]: + clean = name.rsplit(".", 1)[0] + transferred.append({"name": name, "clean": clean, "synced_at": ts}) + return jsonify({"hetzner": hetzner, "pending": pending, "transferred": transferred, + "total_gb": round(sum(f["size_mb"] for f in hetzner + pending) / 1024, 2)}) + @app.route("/downloads") def downloads_page(): - files = [] - for fp in SAVETV_DIR.iterdir(): - if fp.suffix == ".mp4": - st = fp.stat() - size_mb = round(st.st_size / 1024 / 1024, 1) - mtime = st.st_mtime - files.append((fp.name, size_mb, mtime)) - files.sort(key=lambda x: x[2], reverse=True) - total_gb = round(sum(s for _, s, _ in files) / 1024, 2) - nav = '
← Archiv📁 Downloads⚙️ Status
' - rows = "" - from datetime import datetime as _dt - for name, size, mtime in files: - clean = name.rsplit(".", 1)[0] - esc = _html.escape(name, quote=True) - date_str = _dt.fromtimestamp(mtime).strftime("%d.%m.%Y") - href_direct = SAVETV_DIRECT_BASE.rstrip("/") + "/files/" + _urlquote(name) - href_tunnel = SAVETV_TUNNEL_BASE.rstrip("/") + "/files/" + _urlquote(name) - rows += ( - '' - '' + clean + '' - '' + date_str + '' - '' + str(size) + ' MB' - '' - '' - '⬇ Download ' - 'CF ' - '' - '' - ) - return ( - 'Downloads' - '' - + nav + - '

📁 Gespeicherte Filme

' - '
' + str(len(files)) + ' Dateien · ' + str(total_gb) + ' GB
' - '
' - 'Direkt vom Hetzner (ohne Cloudflare). ' - 'Credentials sind im Link eingebettet – Browser-Auth-Dialog sollte nicht erscheinen. ' - 'Fallback-Link CF pro Zeile nutzt den Cloudflare-Tunnel ' - '(' + _html.escape(SAVETV_TUNNEL_BASE, quote=True) + ').' - '
' - '
Sortieren:' - '' - '' - '' - '
' - '' + rows + '
' - '' - ) + return ''' + + + + +SaveTV Pipeline + + + + +

▶ SaveTV Pipeline

+
Lade...
+ +
+
+ ⏰ Wartet auf NAS-Transfer + +
+
Lade...
+
+ +
+
+ 💾 Auf Hetzner bereit + +
+
Lade...
+
+ +
+
+ ✓ Auf deiner NAS + +
+
Lade...
+
+ + + +''' FILMINFO_CACHE = Path("/mnt/savetv/.filminfo_cache.json")