feat(backend): Media-Server-Aufbereitung, echte Webhooks, SMB-Klartext, UHD-Arbeitsverzeichnis, MakeMKV-Key via UI

- 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>
This commit is contained in:
Hitonabi
2026-07-24 08:55:22 +02:00
parent 048562a6ba
commit e032a9df1c
14 changed files with 787 additions and 36 deletions
+66
View File
@@ -0,0 +1,66 @@
"""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]}"
)