diff --git a/homelab-ai-bot/savetv_web.py b/homelab-ai-bot/savetv_web.py new file mode 100644 index 00000000..dbc25fbc --- /dev/null +++ b/homelab-ai-bot/savetv_web.py @@ -0,0 +1,589 @@ +"""Save.TV Web-UI — Film-Archiv durchsuchen und downloaden. + +Läuft auf Port 8765 in CT 116. +Erreichbar via Tailscale: http://100.123.47.7:8765 +""" + +import os +import sys +import json +import threading +from datetime import datetime +from pathlib import Path + +sys.path.insert(0, os.path.dirname(__file__)) + +from flask import Flask, jsonify, render_template_string, request +from tools import savetv + +app = Flask(__name__) + +DOWNLOAD_LOG = Path("/mnt/savetv/.download_log.json") + + +def _load_download_log(): + if DOWNLOAD_LOG.exists(): + try: + return json.loads(DOWNLOAD_LOG.read_text()) + except Exception: + pass + return {} + + +def _save_download_log(log): + try: + DOWNLOAD_LOG.write_text(json.dumps(log, ensure_ascii=False, indent=2)) + except Exception: + pass + + +HTML = r""" + + + + +Save.TV Archiv + + + +
+ +
Lade...
+
+
+
+
+
Lade Archiv von Save.TV...
+
+ +
+
+
0 ausgewählt
+ + + +
+
+ + + +""" + + +@app.route("/") +def index(): + return render_template_string(HTML) + + +@app.route("/api/films") +def api_films(): + entries = savetv._get_full_archive() + seen_titles = {} + series_count = 0 + excluded_count = 0 + + for e in entries: + tc = e.get("STRTELECASTENTRY", {}) + if tc.get("SFOLGE", ""): + series_count += 1 + continue + title = tc.get("STITLE", "?") + if savetv._is_excluded(title): + excluded_count += 1 + continue + + station = tc.get("STVSTATIONNAME", "?") + days_left = int(tc.get("IDAYSLEFTBEFOREDELETE", 0)) + tid = int(tc.get("ITELECASTID", 0)) + is_cinema = savetv._is_known_cinema(title) + key = title.lower().strip() + if key in seen_titles: + if days_left > seen_titles[key]["days_left"]: + seen_titles[key].update(days_left=days_left, tid=tid) + continue + seen_titles[key] = { + "tid": tid, "title": title, "station": station, + "days_left": days_left, "cinema": is_cinema, + } + + films = sorted(seen_titles.values(), key=lambda x: (x["days_left"], not x["cinema"])) + dl_log = _load_download_log() + + return jsonify({ + "films": films, + "total": len(entries), + "kino": sum(1 for f in films if f["cinema"]), + "urgent": sum(1 for f in films if f["days_left"] <= 7), + "downloads": dl_log, + }) + + +@app.route("/api/download", methods=["POST"]) +def api_download(): + data = request.get_json() + tids = data.get("tids", []) + dl_log = _load_download_log() + results = [] + + films_by_tid = {} + entries = savetv._get_full_archive() + for e in entries: + tc = e.get("STRTELECASTENTRY", {}) + tid = int(tc.get("ITELECASTID", 0)) + if tid: + films_by_tid[tid] = tc.get("STITLE", f"film_{tid}") + + def download_one(tid): + title = films_by_tid.get(tid, f"film_{tid}") + filename, err = savetv._download_film(tid, title) + if err: + app.logger.error("Download TID %s: %s", tid, err) + return {"tid": tid, "ok": False, "error": err} + dl_log[str(tid)] = "done" + return {"tid": tid, "ok": True, "filename": filename} + + threads = [] + result_list = [None] * len(tids) + + def worker(i, tid): + result_list[i] = download_one(tid) + + for i, tid in enumerate(tids): + t = threading.Thread(target=worker, args=(i, tid)) + t.daemon = True + t.start() + threads.append(t) + + for t in threads: + t.join(timeout=15) + + results = [r for r in result_list if r] + _save_download_log(dl_log) + + return jsonify({"results": results}) + + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=8765, debug=False)