diff --git a/homelab-ai-bot/tools/savetv.py b/homelab-ai-bot/tools/savetv.py index 966a844a..f9d01c7f 100644 --- a/homelab-ai-bot/tools/savetv.py +++ b/homelab-ai-bot/tools/savetv.py @@ -68,6 +68,17 @@ TOOLS = [ "parameters": {"type": "object", "properties": {}, "required": []}, }, }, + { + "type": "function", + "function": { + "name": "get_savetv_archive_filme", + "description": "Save.TV Archiv-Filme bewerten: Alle fertigen Aufnahmen holen, " + "nach Qualitaet bewerten, deduplizieren. Zeigt Top-Filme, dringende " + "(bald ablaufend) und weitere. Nutze bei 'gute Filme im Archiv', " + "'welche Filme habe ich', 'was ist sehenswert', 'Archiv bewerten'.", + "parameters": {"type": "object", "properties": {}, "required": []}, + }, + }, { "type": "function", "function": { @@ -87,9 +98,10 @@ TOOLS = [ SYSTEM_PROMPT_EXTRA = """TV / Save.TV Tools: - get_savetv_tipps: Zeigt sehenswerte Spielfilme der naechsten Tage/Wochen +- get_savetv_archive_filme: Bewertet alle fertigen Aufnahmen im Archiv nach Qualitaet - savetv_record: Nimmt einen Film per TelecastId auf - get_savetv_status: Zeigt Archiv und geplante Aufnahmen -Wenn der User einen Film aufnehmen will, nutze savetv_record mit der TelecastId. +Wenn der User nach Archiv-Filmen/Bewertung fragt, nutze get_savetv_archive_filme. """ @@ -159,17 +171,21 @@ def _save_seen(seen): def _get_archive(state=0, count=20): - """Archiv abrufen. state: 0=geplant, 1=fertig.""" + """Archiv abrufen (POST, wie Web-UI). state: 0=geplant, 1=fertig.""" s = _get_session() if not s: return {"error": "Login fehlgeschlagen"} try: - r = s.get( + end = datetime.now().strftime("%Y-%m-%d") + start = (datetime.now() - timedelta(days=60)).strftime("%Y-%m-%d") + r = s.post( SAVETV_URL + "/STV/M/obj/archive/JSON/VideoArchiveApi.cfm", - params={ + data={ "bAggregateEntries": "false", "iEntriesPerPage": str(count), "iRecordingState": str(state), + "dStartdate": start, + "dEnddate": end, }, headers={"X-Requested-With": "XMLHttpRequest"}, timeout=15, @@ -179,6 +195,40 @@ def _get_archive(state=0, count=20): return {"error": str(e)} +def _get_full_archive(): + """Alle fertigen Aufnahmen paginiert holen.""" + s = _get_session() + if not s: + return [] + end = datetime.now().strftime("%Y-%m-%d") + start = (datetime.now() - timedelta(days=60)).strftime("%Y-%m-%d") + all_entries = [] + for page in range(1, 20): + try: + r = s.post( + SAVETV_URL + "/STV/M/obj/archive/JSON/VideoArchiveApi.cfm", + data={ + "bAggregateEntries": "false", + "iEntriesPerPage": "100", + "iCurrentPage": str(page), + "iRecordingState": "1", + "dStartdate": start, + "dEnddate": end, + }, + headers={"X-Requested-With": "XMLHttpRequest"}, + timeout=15, + ) + data = r.json() + entries = data.get("ARRVIDEOARCHIVEENTRIES", []) + if not entries: + break + all_entries.extend(entries) + except Exception as e: + log.error("Archive page %d: %s", page, e) + break + return all_entries + + def _scrape_epg(): """Holt Filme aus Save.TV Programmseiten (JSON im HTML). @@ -342,9 +392,154 @@ def _format_film(f, with_tid=True): return "\n".join(lines) +DOKU_KEYWORDS = { + "schlangen", "giftig", "gefährlich", "tiere", "tierwelt", + "wildtiere", "safari", "ozean", "meer", "ozeane", + "doku", "dokumentation", "reportage", "magazin", + "botschafter der meere", "dynastie", "leoparden", + "schildkröten", "wale", "australien", "afrika", + "gehirn unter strom", +} + + +def _score_archive_film(title, station, highlight, subtitle="", thema=""): + """Bewertet einen Archiv-Film heuristisch (0-100).""" + t = title.lower() + s = station.lower() + + if "programmänderung" in t: + return -1 + + for kw in DOKU_KEYWORDS: + if kw in t: + return -1 + + score = 50 + + premium_stations = {"arte", "zdf", "das erste", "mdr", "swr", "ndr", "wdr", "br"} + action_stations = {"prosieben", "sat.1", "kabel 1", "vox", "rtl", "tele 5", "zdf_neo"} + if s in premium_stations: + score += 5 + elif s in action_stations: + score += 3 + + if highlight: + score += 15 + + desc = (thema or subtitle or "").lower() + if len(desc) > 30: + score += 5 + + quality_hints = [ + "oscar", "golden globe", "cannes", "berlinale", "venedig", + "preisgekrönt", "meisterwerk", "bestseller", "basiert auf", + ] + for hint in quality_hints: + if hint in desc or hint in t: + score += 10 + break + + if any(c.isascii() and c.isalpha() for c in title) and not all(c.isascii() for c in title if c.isalpha()): + pass + elif re.search(r'[A-Z][a-z]+ [A-Z][a-z]+', title) and not re.search(r'[äöüÄÖÜß]', title): + score += 8 + + return score + + +def handle_get_savetv_archive_filme(**kw): + """Alle fertigen Archiv-Filme holen, bewerten, deduplizieren, sortiert ausgeben.""" + entries = _get_full_archive() + if not entries: + return "Keine Archiv-Eintraege gefunden." + + films = [] + seen_titles = {} + series_count = 0 + + for e in entries: + tc = e.get("STRTELECASTENTRY", {}) + episode = tc.get("SFOLGE", "") + if episode: + series_count += 1 + continue + + title = tc.get("STITLE", "?") + station = tc.get("STVSTATIONNAME", "?") + highlight = tc.get("BISHIGHLIGHT", False) + subtitle = tc.get("SSUBTITLE", "") + thema = tc.get("STHEMA", "") + date = tc.get("DSTARTDATE", "?")[:10] + days_left = int(tc.get("IDAYSLEFTBEFOREDELETE", 0)) + tid = int(tc.get("ITELECASTID", 0)) + + score = _score_archive_film(title, station, highlight, subtitle, thema) + if score < 0: + continue + + key = title.lower().strip() + if key in seen_titles: + if days_left > seen_titles[key]["days_left"]: + seen_titles[key]["days_left"] = days_left + seen_titles[key]["date"] = date + seen_titles[key]["tid"] = tid + continue + + seen_titles[key] = { + "title": title, "station": station, "date": date, + "days_left": days_left, "score": score, "tid": tid, + "highlight": highlight, + } + + films = sorted(seen_titles.values(), key=lambda x: (-x["score"], x["days_left"])) + + total_archive = len(entries) + urgent = sorted( + [f for f in films if f["days_left"] <= 7], + key=lambda x: (x["days_left"], -x["score"]), + ) + + lines = [ + f"Save.TV Archiv-Bewertung: {len(films)} Filme " + f"(von {total_archive} Aufnahmen, {series_count} Serien-Episoden gefiltert)\n" + ] + + if urgent: + lines.append(f"DRINGEND — {len(urgent)} Filme laufen in <=7 Tagen ab:") + for f in urgent: + hl = " *" if f["highlight"] else "" + lines.append( + f" [{f['days_left']}d] {f['title'][:45]} | {f['station']} | " + f"Score {f['score']}{hl} | TID {f['tid']}" + ) + lines.append("") + + top = [f for f in films if f["score"] >= 60 and f["days_left"] > 7] + if top: + lines.append(f"TOP-FILME ({len(top)}):") + for f in top[:30]: + hl = " *" if f["highlight"] else "" + lines.append( + f" {f['title'][:45]:45s} | {f['station']:12s} | " + f"{f['date']} | {f['days_left']:2d}d | Score {f['score']}{hl}" + ) + lines.append("") + + rest = [f for f in films if f["score"] < 60 and f["days_left"] > 7] + if rest: + lines.append(f"WEITERE ({len(rest)}):") + for f in rest[:20]: + lines.append( + f" {f['title'][:45]:45s} | {f['station']:12s} | " + f"{f['date']} | {f['days_left']:2d}d" + ) + + return "\n".join(lines) + + def handle_get_savetv_status(**kw): - archive = _get_archive(state=1, count=5) - planned = _get_archive(state=0, count=10) + archive = _get_archive(state=1, count=20) + planned = _get_archive(state=0, count=20) if "error" in archive: return "Save.TV Fehler: " + archive["error"] @@ -352,12 +547,13 @@ def handle_get_savetv_status(**kw): lines = ["Save.TV Status\n"] total = int(archive.get("ITOTALENTRIESINARCHIVE", 0)) - lines.append("Archiv: " + str(total) + " Aufnahmen gesamt") + fertig_total = int(archive.get("ITOTALENTRIES", 0)) + lines.append(f"Archiv: {total} Aufnahmen gesamt, {fertig_total} fertig") fertig = archive.get("ARRVIDEOARCHIVEENTRIES", []) if fertig: lines.append("\nLetzte fertige Aufnahmen:") - for e in fertig[:5]: + for e in fertig[:10]: tc = e.get("STRTELECASTENTRY", {}) lines.append( " " + tc.get("STITLE", "?")[:40] + " | " @@ -368,7 +564,7 @@ def handle_get_savetv_status(**kw): geplant = planned.get("ARRVIDEOARCHIVEENTRIES", []) plan_total = int(planned.get("ITOTALENTRIES", 0)) if geplant: - lines.append("\nGeplante Aufnahmen (" + str(plan_total) + "):") + lines.append(f"\nGeplante Aufnahmen ({plan_total}):") for e in geplant[:10]: tc = e.get("STRTELECASTENTRY", {}) lines.append( @@ -455,5 +651,6 @@ def handle_savetv_record(telecast_id=0, **kw): HANDLERS = { "get_savetv_status": handle_get_savetv_status, "get_savetv_tipps": handle_get_savetv_tipps, + "get_savetv_archive_filme": handle_get_savetv_archive_filme, "savetv_record": handle_savetv_record, }