e032a9df1c
- jobs.meta (Migration in beiden db.py): Disc-Metadaten wandern in den Job —
Quelle fuer Job-Detail-Popup (GET /jobs/{id}/detail), Ordner-Benennung, NFO.
- medien.py (Worker, mit Tests): Zielordner 'Titel (Jahr)' statt Job-UUID;
movie.nfo/tvshow.nfo (Kodi-Schema, kodi.wiki/view/NFO_files) + poster.jpg
fuer jellyfin/emby/kodi; plex nur Benennung; Kollision -> Job-ID-Suffix.
- notify.py (identisch in API+Worker, mit Tests): Webhook bei Job-Ende —
Discord ({content}, discord.com/developers), Slack ({text},
api.slack.com/messaging/webhooks), ntfy (Rohtext + ?title=, docs.ntfy.sh),
generisches JSON. Vorher war das Setting ein Placebo: nichts sendete je.
POST /notifications/test beweist die Anbindung sofort.
- mounts.py: NT_STATUS-Fehler -> handelbarer Klartext (ACCESS_DENIED ohne
Credentials = Gast-Abfrage verweigert), Timeout-Meldung, mount-Hinweis
bei error(13). Mit Tests.
- tasks.py: Platz-Check per Disc-Groesse (ioctl BLKGETSIZE64) VOR dem Rip;
Arbeitsverzeichnis workDir (unter /app/media, z.B. NAS) statt fix
/app/temp/raw — 4K-UHD-Rohdaten (bis 100 GB) sprengen sonst die VM-Platte;
MakeMKV-Key aus UI-Settings (~/.MakeMKV/settings.conf, Format wie
entrypoint.sh) gilt ab dem naechsten Rip ohne Rebuild.
- caps.py: Werkzeug-Versionen (MakeMKV aus ENV MAKEMKV_VERSION im Image,
makemkvcon hat keinen --version-Schalter lt. usage.txt; HandBrakeCLI
--version lt. handbrake.fr/docs) + Key-Quelle; workers.info-Spalte.
- /browse liefert jetzt auch DATEIEN (Name + Groesse) — der 'leere'
bluray-Ordner war voll, der Browser zeigte nur Unterordner.
- GET /system/info: Versionen, Plattenplatz, Key-/Webhook-Status.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
67 lines
2.5 KiB
Python
67 lines
2.5 KiB
Python
"""Webhook-Benachrichtigungen bei Job-Ende (Discord, Slack, ntfy, generisch).
|
|
|
|
Bis 24.07. war das notificationWebhook-Setting ein Placebo: das UI speicherte
|
|
die URL, aber NICHTS hat je gesendet. Jetzt meldet der Worker Job-Ende
|
|
(fertig/fehlgeschlagen/abgebrochen) und die API bietet einen Test-Endpoint.
|
|
|
|
Das Modul existiert bewusst identisch in API und Worker
|
|
(docker/api/notify.py) — es gibt kein geteiltes Paket zwischen den Containern.
|
|
Wer es ändert, ändert BEIDE Dateien.
|
|
|
|
Payload-Formate (dokumentiert, nicht geraten — AGENTS Regel D):
|
|
- Discord: POST JSON {"content": "..."} — discord.com/developers/docs/resources/webhook
|
|
- Slack: POST JSON {"text": "..."} — api.slack.com/messaging/webhooks
|
|
- ntfy: POST Roh-Text an https://ntfy.sh/<topic>, Titel via ?title= —
|
|
docs.ntfy.sh/publish
|
|
- Generisch: POST JSON {"title", "message", "level"} für eigene Empfänger
|
|
(Home Assistant, n8n, eigene Skripte).
|
|
"""
|
|
|
|
import requests
|
|
|
|
TIMEOUT_SEKUNDEN = 10
|
|
|
|
|
|
def erkenne_webhook_typ(url: str) -> str:
|
|
"""Erkennt den Dienst an der URL — der Nutzer muss nichts konfigurieren."""
|
|
u = url.lower()
|
|
if "discord.com/api/webhooks" in u or "discordapp.com/api/webhooks" in u:
|
|
return "discord"
|
|
if "hooks.slack.com" in u:
|
|
return "slack"
|
|
if "ntfy" in u:
|
|
return "ntfy"
|
|
return "generisch"
|
|
|
|
|
|
def baue_payload(url: str, titel: str, text: str, level: str = "info"):
|
|
"""Pure Funktion (testbar): (typ, json_payload) — ntfy sendet Roh-Text."""
|
|
typ = erkenne_webhook_typ(url)
|
|
if typ == "discord":
|
|
return typ, {"content": f"**{titel}**\n{text}"}
|
|
if typ == "slack":
|
|
return typ, {"text": f"*{titel}*\n{text}"}
|
|
if typ == "ntfy":
|
|
return typ, None
|
|
return typ, {"title": titel, "message": text, "level": level}
|
|
|
|
|
|
def sende(url: str, titel: str, text: str, level: str = "info") -> None:
|
|
"""Schickt die Nachricht; wirft RuntimeError mit Klartext bei Fehlern."""
|
|
typ, payload = baue_payload(url, titel, text, level)
|
|
try:
|
|
if typ == "ntfy":
|
|
antwort = requests.post(
|
|
url, data=text.encode("utf-8"),
|
|
params={"title": titel}, timeout=TIMEOUT_SEKUNDEN,
|
|
)
|
|
else:
|
|
antwort = requests.post(url, json=payload, timeout=TIMEOUT_SEKUNDEN)
|
|
except requests.RequestException as e:
|
|
raise RuntimeError(f"Webhook nicht erreichbar: {e}") from e
|
|
if antwort.status_code >= 300:
|
|
raise RuntimeError(
|
|
f"Webhook antwortete mit HTTP {antwort.status_code}: "
|
|
f"{(antwort.text or '')[:200]}"
|
|
)
|