feat: Save.TV Integration — EPG-Scanner, Filmtipps, Aufnahme per Inline-Button + täglicher Cronjob 14:00
This commit is contained in:
parent
9e660d8c79
commit
eeec98303d
5 changed files with 442 additions and 1 deletions
Binary file not shown.
Binary file not shown.
|
|
@ -112,14 +112,17 @@ logging.basicConfig(
|
||||||
log = logging.getLogger("hausmeister")
|
log = logging.getLogger("hausmeister")
|
||||||
|
|
||||||
ALLOWED_CHAT_IDS: set[int] = set()
|
ALLOWED_CHAT_IDS: set[int] = set()
|
||||||
|
CHAT_ID: int | None = None
|
||||||
|
|
||||||
|
|
||||||
def _load_token_and_chat():
|
def _load_token_and_chat():
|
||||||
|
global CHAT_ID
|
||||||
cfg = config.parse_config()
|
cfg = config.parse_config()
|
||||||
token = cfg.raw.get("TG_HAUSMEISTER_TOKEN", "")
|
token = cfg.raw.get("TG_HAUSMEISTER_TOKEN", "")
|
||||||
chat_id = cfg.raw.get("TG_CHAT_ID", "")
|
chat_id = cfg.raw.get("TG_CHAT_ID", "")
|
||||||
if chat_id:
|
if chat_id:
|
||||||
ALLOWED_CHAT_IDS.add(int(chat_id))
|
CHAT_ID = int(chat_id)
|
||||||
|
ALLOWED_CHAT_IDS.add(CHAT_ID)
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -725,6 +728,82 @@ async def handle_message(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||||
await update.message.reply_text(f"Fehler: {e}")
|
await update.message.reply_text(f"Fehler: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
async def handle_callback(update: Update, ctx: ContextTypes.DEFAULT_TYPE):
|
||||||
|
"""Inline-Button Callbacks (z.B. Save.TV Aufnahme)."""
|
||||||
|
query = update.callback_query
|
||||||
|
await query.answer()
|
||||||
|
data = query.data or ""
|
||||||
|
|
||||||
|
if data.startswith("savetv_rec_"):
|
||||||
|
tid = data.replace("savetv_rec_", "")
|
||||||
|
try:
|
||||||
|
from tools import savetv
|
||||||
|
result = savetv.handle_savetv_record(telecast_id=int(tid))
|
||||||
|
await query.edit_message_text(
|
||||||
|
query.message.text + f"\n\n✅ {result}"
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
log.exception("Save.TV Aufnahme Fehler")
|
||||||
|
await query.edit_message_text(
|
||||||
|
query.message.text + f"\n\n❌ Fehler: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
elif data.startswith("savetv_skip_"):
|
||||||
|
await query.edit_message_text(
|
||||||
|
query.message.text + "\n\n⏭ Übersprungen"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _send_daily_filmtipps(app_context: ContextTypes.DEFAULT_TYPE):
|
||||||
|
"""Täglicher Cronjob: Filmtipps via Telegram senden."""
|
||||||
|
if not CHAT_ID:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
from tools import savetv
|
||||||
|
telecasts = savetv._scrape_epg()
|
||||||
|
if not telecasts:
|
||||||
|
return
|
||||||
|
films = savetv._filter_films(telecasts)
|
||||||
|
if not films:
|
||||||
|
return
|
||||||
|
|
||||||
|
header = f"🎬 TV-Filmtipps für heute ({datetime.now().strftime('%d.%m.%Y')})\n"
|
||||||
|
await app_context.bot.send_message(chat_id=CHAT_ID, text=header)
|
||||||
|
|
||||||
|
for f in films[:6]:
|
||||||
|
tid = int(f.get("ITELECASTID", 0))
|
||||||
|
title = f.get("STITLE", "?")
|
||||||
|
station = f.get("STVSTATIONNAME", "?")
|
||||||
|
start = f.get("DSTARTDATE", "?")[:16]
|
||||||
|
subcat = f.get("SSUBCATEGORYNAME", "")
|
||||||
|
desc = (f.get("STHEMA") or f.get("SFULLSUBTITLE") or "")[:150]
|
||||||
|
recorded = " ✅ Bereits geplant" if f.get("BEXISTRECORD") else ""
|
||||||
|
|
||||||
|
text = f"🎬 *{title}*{recorded}\n📺 {station} | ⏰ {start}\n🎭 {subcat}"
|
||||||
|
if desc:
|
||||||
|
text += f"\n_{desc}_"
|
||||||
|
|
||||||
|
keyboard = InlineKeyboardMarkup([
|
||||||
|
[
|
||||||
|
InlineKeyboardButton("🔴 Aufnehmen", callback_data=f"savetv_rec_{tid}"),
|
||||||
|
InlineKeyboardButton("⏭ Nein", callback_data=f"savetv_skip_{tid}"),
|
||||||
|
]
|
||||||
|
])
|
||||||
|
await app_context.bot.send_message(
|
||||||
|
chat_id=CHAT_ID,
|
||||||
|
text=text,
|
||||||
|
reply_markup=keyboard,
|
||||||
|
parse_mode="Markdown",
|
||||||
|
)
|
||||||
|
|
||||||
|
log.info("Tägliche Filmtipps gesendet: %d Filme", min(len(films), 6))
|
||||||
|
except Exception:
|
||||||
|
log.exception("Fehler beim Senden der Filmtipps")
|
||||||
|
|
||||||
|
|
||||||
|
from datetime import datetime, time as dtime
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
token = _load_token_and_chat()
|
token = _load_token_and_chat()
|
||||||
if not token:
|
if not token:
|
||||||
|
|
@ -749,11 +828,19 @@ def main():
|
||||||
app.add_handler(CommandHandler("check", cmd_check))
|
app.add_handler(CommandHandler("check", cmd_check))
|
||||||
app.add_handler(CommandHandler("feeds", cmd_feeds))
|
app.add_handler(CommandHandler("feeds", cmd_feeds))
|
||||||
app.add_handler(CommandHandler("memory", cmd_memory))
|
app.add_handler(CommandHandler("memory", cmd_memory))
|
||||||
|
app.add_handler(CallbackQueryHandler(handle_callback))
|
||||||
app.add_handler(MessageHandler(filters.VOICE, handle_voice))
|
app.add_handler(MessageHandler(filters.VOICE, handle_voice))
|
||||||
app.add_handler(MessageHandler(filters.PHOTO, handle_photo))
|
app.add_handler(MessageHandler(filters.PHOTO, handle_photo))
|
||||||
app.add_handler(MessageHandler(filters.Document.ALL, handle_document))
|
app.add_handler(MessageHandler(filters.Document.ALL, handle_document))
|
||||||
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
|
app.add_handler(MessageHandler(filters.TEXT & ~filters.COMMAND, handle_message))
|
||||||
|
|
||||||
|
app.job_queue.run_daily(
|
||||||
|
_send_daily_filmtipps,
|
||||||
|
time=dtime(hour=14, minute=0),
|
||||||
|
name="daily_filmtipps",
|
||||||
|
)
|
||||||
|
log.info("Täglicher Filmtipp-Job registriert (14:00 Uhr)")
|
||||||
|
|
||||||
async def post_init(application):
|
async def post_init(application):
|
||||||
await application.bot.set_my_commands(BOT_COMMANDS)
|
await application.bot.set_my_commands(BOT_COMMANDS)
|
||||||
log.info("Kommandomenü registriert")
|
log.info("Kommandomenü registriert")
|
||||||
|
|
|
||||||
349
homelab-ai-bot/tools/savetv.py
Normal file
349
homelab-ai-bot/tools/savetv.py
Normal file
|
|
@ -0,0 +1,349 @@
|
||||||
|
"""Save.TV Online-Videorecorder — EPG Scanner + Film-Tipps + Aufnahme-Steuerung.
|
||||||
|
|
||||||
|
Architektur:
|
||||||
|
- EPG-Daten kommen von Save.TV TvProgramm-Seiten (eingebettetes JSON)
|
||||||
|
- Nur TVCATEGORYID 1 (Spielfilm) wird beachtet
|
||||||
|
- LLM bewertet Filme per Titel + Beschreibung + Genre
|
||||||
|
- Aufnahmen werden per tcJWriteRecord.cfm angelegt
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import requests
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
log = logging.getLogger("savetv")
|
||||||
|
|
||||||
|
SAVETV_URL = "https://www.save.tv"
|
||||||
|
SAVETV_USER = ""
|
||||||
|
SAVETV_PASS = ""
|
||||||
|
|
||||||
|
_session = None
|
||||||
|
_session_ts = None
|
||||||
|
SESSION_MAX_AGE = 1800
|
||||||
|
|
||||||
|
EPG_PAGES = [
|
||||||
|
"/STV/M/obj/TVProgCtr/TvProgramm2015.cfm",
|
||||||
|
"/STV/M/obj/TVProgCtr/TvProgramm2215.cfm",
|
||||||
|
]
|
||||||
|
|
||||||
|
SPAM_SUBCATEGORIES = {
|
||||||
|
"teleshop", "shopping", "dauerwerbesendung", "volksmusik",
|
||||||
|
"casting", "reality", "quiz/spiel", "comic", "zeichentrick",
|
||||||
|
"erotik", "kindersendung",
|
||||||
|
}
|
||||||
|
|
||||||
|
GOOD_SUBCATEGORIES = {
|
||||||
|
"action", "thriller", "krimi", "drama", "komödie", "komodie",
|
||||||
|
"science fiction", "sci-fi", "fantasy", "abenteuer", "horror",
|
||||||
|
"western", "historienfilm", "animation", "mystery",
|
||||||
|
}
|
||||||
|
|
||||||
|
TOOLS = [
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "get_savetv_status",
|
||||||
|
"description": "Save.TV Status: Aufnahmen im Archiv, geplante Aufnahmen anzeigen.",
|
||||||
|
"parameters": {"type": "object", "properties": {}, "required": []},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "get_savetv_tipps",
|
||||||
|
"description": "TV-Filmtipps: Sehenswerte Spielfilme aus dem heutigen TV-Programm. "
|
||||||
|
"Nutze bei 'was laeuft heute', 'gute Filme', 'TV Tipps', 'Fernsehen', 'Save.TV'.",
|
||||||
|
"parameters": {"type": "object", "properties": {}, "required": []},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "function",
|
||||||
|
"function": {
|
||||||
|
"name": "savetv_record",
|
||||||
|
"description": "Save.TV Aufnahme anlegen fuer eine bestimmte TelecastId. "
|
||||||
|
"Nutze wenn User sagt 'nimm auf', 'aufnehmen', 'record'.",
|
||||||
|
"parameters": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"telecast_id": {"type": "number", "description": "TelecastId der Sendung"}
|
||||||
|
},
|
||||||
|
"required": ["telecast_id"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
SYSTEM_PROMPT_EXTRA = """TV / Save.TV Tools:
|
||||||
|
- get_savetv_tipps: Zeigt sehenswerte Spielfilme aus dem heutigen TV-Programm
|
||||||
|
- 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.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _init_creds():
|
||||||
|
global SAVETV_USER, SAVETV_PASS
|
||||||
|
if SAVETV_USER:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
from core import config
|
||||||
|
cfg = config.parse_config()
|
||||||
|
SAVETV_USER = cfg.raw.get("SAVETV_USER", "")
|
||||||
|
SAVETV_PASS = cfg.raw.get("SAVETV_PASS", "")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def _get_session() -> requests.Session | None:
|
||||||
|
"""Login und Session cachen."""
|
||||||
|
global _session, _session_ts
|
||||||
|
_init_creds()
|
||||||
|
|
||||||
|
now = datetime.now()
|
||||||
|
if _session and _session_ts and (now - _session_ts).seconds < SESSION_MAX_AGE:
|
||||||
|
return _session
|
||||||
|
|
||||||
|
s = requests.Session()
|
||||||
|
s.headers.update({"User-Agent": "Mozilla/5.0 Hausmeister-Bot/1.0"})
|
||||||
|
|
||||||
|
try:
|
||||||
|
r = s.post(
|
||||||
|
f"{SAVETV_URL}/STV/M/Index.cfm?sk=PREMIUM",
|
||||||
|
data={"sUsername": SAVETV_USER, "sPassword": SAVETV_PASS, "value": "Login"},
|
||||||
|
allow_redirects=True,
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
cookies = s.cookies.get_dict()
|
||||||
|
if not cookies.get("savetv_active_login"):
|
||||||
|
log.warning("Save.TV Login fehlgeschlagen (kein savetv_active_login Cookie)")
|
||||||
|
return None
|
||||||
|
except Exception as e:
|
||||||
|
log.error("Save.TV Login Error: %s", e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
_session = s
|
||||||
|
_session_ts = now
|
||||||
|
log.info("Save.TV Login erfolgreich")
|
||||||
|
return s
|
||||||
|
|
||||||
|
|
||||||
|
def _get_archive(state: int = 0, count: int = 20) -> dict:
|
||||||
|
"""Archiv abrufen. state: 0=geplant, 1=fertig."""
|
||||||
|
s = _get_session()
|
||||||
|
if not s:
|
||||||
|
return {"error": "Login fehlgeschlagen"}
|
||||||
|
try:
|
||||||
|
r = s.get(
|
||||||
|
f"{SAVETV_URL}/STV/M/obj/archive/JSON/VideoArchiveApi.cfm",
|
||||||
|
params={
|
||||||
|
"bAggregateEntries": "false",
|
||||||
|
"iEntriesPerPage": str(count),
|
||||||
|
"iRecordingState": str(state),
|
||||||
|
},
|
||||||
|
headers={"X-Requested-With": "XMLHttpRequest"},
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
return r.json()
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
|
||||||
|
def _scrape_epg() -> list[dict]:
|
||||||
|
"""Holt Filme aus den Save.TV Programmseiten (JSON im HTML)."""
|
||||||
|
s = _get_session()
|
||||||
|
if not s:
|
||||||
|
return []
|
||||||
|
|
||||||
|
all_telecasts = []
|
||||||
|
seen_ids = set()
|
||||||
|
|
||||||
|
for page_path in EPG_PAGES:
|
||||||
|
try:
|
||||||
|
r = s.get(f"{SAVETV_URL}{page_path}", timeout=15)
|
||||||
|
m = re.search(
|
||||||
|
r'model\s*=\s*(\{"TvCategoryId".*?"SortedTelecasts":\[.*?\]\})',
|
||||||
|
r.text,
|
||||||
|
re.DOTALL,
|
||||||
|
)
|
||||||
|
if not m:
|
||||||
|
log.warning("Kein model-JSON in %s", page_path)
|
||||||
|
continue
|
||||||
|
|
||||||
|
data = json.loads(m.group(1))
|
||||||
|
for tc in data.get("SortedTelecasts", []):
|
||||||
|
tid = int(tc.get("ITELECASTID", 0))
|
||||||
|
if tid and tid not in seen_ids:
|
||||||
|
seen_ids.add(tid)
|
||||||
|
all_telecasts.append(tc)
|
||||||
|
except Exception as e:
|
||||||
|
log.error("EPG Scrape %s: %s", page_path, e)
|
||||||
|
|
||||||
|
log.info("EPG: %d Sendungen gesamt", len(all_telecasts))
|
||||||
|
return all_telecasts
|
||||||
|
|
||||||
|
|
||||||
|
def _filter_films(telecasts: list[dict]) -> list[dict]:
|
||||||
|
"""Filtert auf Spielfilme und bewertet sie."""
|
||||||
|
films = []
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
for tc in telecasts:
|
||||||
|
cat_id = tc.get("TVCATEGORYID", 0)
|
||||||
|
if cat_id != 1.0:
|
||||||
|
continue
|
||||||
|
|
||||||
|
title = tc.get("STITLE", "")
|
||||||
|
if not title or len(title) < 2:
|
||||||
|
continue
|
||||||
|
|
||||||
|
subcat = (tc.get("SSUBCATEGORYNAME") or "").lower()
|
||||||
|
if subcat in SPAM_SUBCATEGORIES:
|
||||||
|
continue
|
||||||
|
|
||||||
|
start_str = tc.get("DSTARTDATE", "")
|
||||||
|
try:
|
||||||
|
start_dt = datetime.strptime(start_str, "%Y-%m-%d %H:%M:%S")
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
|
||||||
|
if start_dt < now:
|
||||||
|
continue
|
||||||
|
|
||||||
|
score = 50
|
||||||
|
if subcat in GOOD_SUBCATEGORIES:
|
||||||
|
score += 20
|
||||||
|
|
||||||
|
hour = start_dt.hour
|
||||||
|
if 20 <= hour <= 22:
|
||||||
|
score += 15
|
||||||
|
elif 14 <= hour <= 19:
|
||||||
|
score += 5
|
||||||
|
|
||||||
|
desc = tc.get("STHEMA") or tc.get("SFULLSUBTITLE") or ""
|
||||||
|
if len(desc) > 50:
|
||||||
|
score += 10
|
||||||
|
|
||||||
|
already_recorded = tc.get("BEXISTRECORD", False)
|
||||||
|
if already_recorded:
|
||||||
|
score -= 30
|
||||||
|
|
||||||
|
tc["_score"] = score
|
||||||
|
tc["_start_dt"] = start_dt
|
||||||
|
films.append(tc)
|
||||||
|
|
||||||
|
films.sort(key=lambda x: (-x["_score"], x["_start_dt"]))
|
||||||
|
return films
|
||||||
|
|
||||||
|
|
||||||
|
def _record_telecast(telecast_id: int) -> str:
|
||||||
|
"""Aufnahme anlegen."""
|
||||||
|
s = _get_session()
|
||||||
|
if not s:
|
||||||
|
return "Login fehlgeschlagen"
|
||||||
|
try:
|
||||||
|
r = s.post(
|
||||||
|
f"{SAVETV_URL}/STV/M/obj/TC/tcJWriteRecord.cfm",
|
||||||
|
data={"TelecastId": telecast_id, "iRecordingBuffer": 0},
|
||||||
|
headers={"X-Requested-With": "XMLHttpRequest"},
|
||||||
|
timeout=15,
|
||||||
|
)
|
||||||
|
data = r.json()
|
||||||
|
return data.get("SMESSAGE", "Unbekannte Antwort")
|
||||||
|
except Exception as e:
|
||||||
|
return f"Fehler: {e}"
|
||||||
|
|
||||||
|
|
||||||
|
def handle_get_savetv_status(**kw):
|
||||||
|
archive = _get_archive(state=1, count=5)
|
||||||
|
planned = _get_archive(state=0, count=10)
|
||||||
|
|
||||||
|
if "error" in archive:
|
||||||
|
return f"Save.TV Fehler: {archive['error']}"
|
||||||
|
|
||||||
|
lines = ["📺 Save.TV Status\n"]
|
||||||
|
|
||||||
|
total = int(archive.get("ITOTALENTRIESINARCHIVE", 0))
|
||||||
|
lines.append(f"Archiv: {total} Aufnahmen gesamt")
|
||||||
|
|
||||||
|
fertig = archive.get("ARRVIDEOARCHIVEENTRIES", [])
|
||||||
|
if fertig:
|
||||||
|
lines.append("\n🎬 Letzte fertige Aufnahmen:")
|
||||||
|
for e in fertig[:5]:
|
||||||
|
tc = e.get("STRTELECASTENTRY", {})
|
||||||
|
lines.append(
|
||||||
|
f" • {tc.get('STITLE', '?')[:40]} | "
|
||||||
|
f"{tc.get('DSTARTDATE', '?')[:10]} | "
|
||||||
|
f"{tc.get('STVSTATIONNAME', '?')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
geplant = planned.get("ARRVIDEOARCHIVEENTRIES", [])
|
||||||
|
plan_total = int(planned.get("ITOTALENTRIES", 0))
|
||||||
|
if geplant:
|
||||||
|
lines.append(f"\n⏰ Geplante Aufnahmen ({plan_total}):")
|
||||||
|
for e in geplant[:10]:
|
||||||
|
tc = e.get("STRTELECASTENTRY", {})
|
||||||
|
lines.append(
|
||||||
|
f" • {tc.get('STITLE', '?')[:40]} | "
|
||||||
|
f"{tc.get('DSTARTDATE', '?')[:16]} | "
|
||||||
|
f"{tc.get('STVSTATIONNAME', '?')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_get_savetv_tipps(**kw):
|
||||||
|
telecasts = _scrape_epg()
|
||||||
|
if not telecasts:
|
||||||
|
return "Konnte keine Programmdaten von Save.TV laden."
|
||||||
|
|
||||||
|
films = _filter_films(telecasts)
|
||||||
|
if not films:
|
||||||
|
return "Keine sehenswerten Spielfilme im heutigen Programm gefunden."
|
||||||
|
|
||||||
|
lines = ["🎬 TV-Filmtipps heute\n"]
|
||||||
|
for f in films[:8]:
|
||||||
|
subcat = f.get("SSUBCATEGORYNAME", "")
|
||||||
|
station = f.get("STVSTATIONNAME", "?")
|
||||||
|
start = f.get("DSTARTDATE", "?")[:16]
|
||||||
|
title = f.get("STITLE", "?")
|
||||||
|
subtitle = f.get("SFULLSUBTITLE") or f.get("SSUBTITLE") or ""
|
||||||
|
desc = f.get("STHEMA") or ""
|
||||||
|
tid = int(f.get("ITELECASTID", 0))
|
||||||
|
recorded = "✅" if f.get("BEXISTRECORD") else ""
|
||||||
|
|
||||||
|
lines.append(f"🎬 {title} {recorded}")
|
||||||
|
if subtitle and subtitle != title:
|
||||||
|
lines.append(f" {subtitle[:60]}")
|
||||||
|
lines.append(f" 📺 {station} | ⏰ {start} | 🎭 {subcat}")
|
||||||
|
if desc and len(desc) > 10:
|
||||||
|
lines.append(f" {desc[:120]}...")
|
||||||
|
lines.append(f" → Aufnahme: TelecastId {tid}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
|
lines.append("💡 Sage 'Nimm [Filmname] auf' oder 'Aufnahme TelecastId XXXXX'")
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_savetv_record(telecast_id=0, **kw):
|
||||||
|
if not telecast_id:
|
||||||
|
return "Keine TelecastId angegeben."
|
||||||
|
tid = int(telecast_id)
|
||||||
|
|
||||||
|
telecasts = _scrape_epg()
|
||||||
|
title = f"ID {tid}"
|
||||||
|
for tc in telecasts:
|
||||||
|
if int(tc.get("ITELECASTID", 0)) == tid:
|
||||||
|
title = tc.get("STITLE", title)
|
||||||
|
break
|
||||||
|
|
||||||
|
result = _record_telecast(tid)
|
||||||
|
return f"📺 Save.TV: {result}\n🎬 Sendung: {title}"
|
||||||
|
|
||||||
|
|
||||||
|
HANDLERS = {
|
||||||
|
"get_savetv_status": handle_get_savetv_status,
|
||||||
|
"get_savetv_tipps": handle_get_savetv_tipps,
|
||||||
|
"savetv_record": handle_savetv_record,
|
||||||
|
}
|
||||||
|
|
@ -196,6 +196,11 @@ MCP_PATH="/root/homelab-mcp"
|
||||||
MCP_VENV="/root/homelab-mcp/.venv"
|
MCP_VENV="/root/homelab-mcp/.venv"
|
||||||
MCP_TOOLS="homelab_overview,homelab_all_containers,homelab_container_status,homelab_query_logs,homelab_get_errors,homelab_check_silence,homelab_host_health,homelab_metrics,homelab_get_config,homelab_loki_labels,homelab_prometheus_targets"
|
MCP_TOOLS="homelab_overview,homelab_all_containers,homelab_container_status,homelab_query_logs,homelab_get_errors,homelab_check_silence,homelab_host_health,homelab_metrics,homelab_get_config,homelab_loki_labels,homelab_prometheus_targets"
|
||||||
|
|
||||||
|
# --- SAVE.TV (Online-Videorecorder) ---
|
||||||
|
SAVETV_USER="739281"
|
||||||
|
SAVETV_PASS="Astral1966"
|
||||||
|
SAVETV_URL="https://www.save.tv"
|
||||||
|
|
||||||
# --- E-MAIL (All-Inkl IMAP-Spiegel von GMX) ---
|
# --- E-MAIL (All-Inkl IMAP-Spiegel von GMX) ---
|
||||||
MAIL_IMAP_SERVER="w0206aa8.kasserver.com"
|
MAIL_IMAP_SERVER="w0206aa8.kasserver.com"
|
||||||
MAIL_IMAP_PORT="993"
|
MAIL_IMAP_PORT="993"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue