From 3f47b504c287d259cc13d590869e547e83f1aefb Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Fri, 24 Jul 2026 21:27:38 +0200 Subject: [PATCH] feat(api): MakeMKV-Beta-Key automatisch erneuern Der kostenlose Beta-Key wechselt ~monatlich und laeuft zum Monatsende ab; bisher musste er von Hand nachgetragen werden, sonst blockt Blu-ray-Ripping irgendwann still. Neu: taeglicher Forum-Abgleich (t=1053) -> Key in die Settings (makemkvAppKey). Der Worker liest ihn VOR jedem Rip (tasks.py) -> wirkt ohne Rebuild ab dem naechsten Rip. - docker/api/makemkv_key.py: extract_key (pure, testbar) + fetch_current_key + refresh_once/refresh_loop; loggt in die App-Logs (add_log). - docker/api/main.py: refresh_loop als startup-Task. - docker/api/test_makemkv_key.py: Parser-Tests - fingen den Bug, dass Beta-Keys laenger als 64 Zeichen sind (Regex {50,} statt {64}). Betrifft nur die Software-Lizenz (oeffentlicher Gratis-Key), NICHT Disc-Schluessel - die zieht MakeMKV via LibreDrive selbst. Ruff gruen, 4 Tests gruen, Live-Fetch gegen das echte Forum verifiziert. Co-Authored-By: Claude Opus 4.8 --- docker/api/main.py | 5 +++ docker/api/makemkv_key.py | 81 ++++++++++++++++++++++++++++++++++ docker/api/test_makemkv_key.py | 28 ++++++++++++ 3 files changed, 114 insertions(+) create mode 100644 docker/api/makemkv_key.py create mode 100644 docker/api/test_makemkv_key.py diff --git a/docker/api/main.py b/docker/api/main.py index 932d25b..7eb41ad 100644 --- a/docker/api/main.py +++ b/docker/api/main.py @@ -12,6 +12,7 @@ import uuid import db import devices as device_discovery +import makemkv_key import mounts as mount_verwaltung import notify from celery_client import celery_client, start_rip @@ -53,6 +54,10 @@ async def startup_event(): await asyncio.to_thread(remount) asyncio.create_task(disc_watcher()) + # MakeMKV-Beta-Key automatisch aktuell halten (wechselt ~monatlich, laeuft zum + # Monatsende ab) — sonst blockt Blu-ray-Ripping irgendwann still. Taeglicher + # Forum-Abgleich; wirkt ohne Rebuild ab dem naechsten Rip. + asyncio.create_task(makemkv_key.refresh_loop()) # Auto-Pre-Scan-Ergebnisse je Laufwerk: das Dashboard zeigt damit sofort, diff --git a/docker/api/makemkv_key.py b/docker/api/makemkv_key.py new file mode 100644 index 0000000..6ae9953 --- /dev/null +++ b/docker/api/makemkv_key.py @@ -0,0 +1,81 @@ +"""Automatische Erneuerung des MakeMKV-Beta-Keys. + +Der kostenlose MakeMKV-Beta-Key (Forum-Thread "MakeMKV is free while in beta", +t=1053) wechselt etwa monatlich und laeuft zum Monatsende ab. Ohne gueltigen Key +faellt Blu-ray/UHD-Ripping in den 30-Tage-Testmodus; DVD-Ripping bleibt frei. +Bisher musste der Key von Hand nachgetragen werden (UI: Einstellungen -> System). + +Dieses Modul holt den aktuellen Key vom Forum und schreibt ihn in die Settings +(makemkvAppKey). Der Worker liest den Key VOR jedem Rip aus den Settings +(docker/worker/tasks.py) und schreibt settings.conf -> die Erneuerung wirkt ohne +Rebuild/Neustart, ab dem naechsten Rip. + +Wichtig: Das ist die kostenlose, oeffentliche Beta-LIZENZ der Software selbst — +NICHT das Entschluesseln oder Verteilen von Disc-Schluesseln. Den AACS-Schluessel +zieht MakeMKV via LibreDrive ohnehin selbst aus dem Laufwerk. + +Quelle/Format dokumentiert (AGENTS Regel D — nicht geraten): +- Forum-Thread: https://forum.makemkv.com/forum/viewtopic.php?t=1053 +- Key-Format: "T-" + langer String aus [A-Za-z0-9_] (typ. ~66 Zeichen; die exakte + Laenge variiert je Key, darum kein fixes Limit, sondern mind. 50, greedy). +""" +import asyncio +import re + +FORUM_URL = "https://forum.makemkv.com/forum/viewtopic.php?t=1053" +KEY_RE = re.compile(r"T-[A-Za-z0-9_]{50,}") +_HEADERS = {"User-Agent": "rippy-makemkv-key-refresh/1.0"} + + +def extract_key(html: str) -> str | None: + """Zieht den Beta-Key ("T-...") aus dem Forum-HTML. Pure Funktion -> testbar.""" + treffer = KEY_RE.search(html or "") + return treffer.group(0) if treffer else None + + +def fetch_current_key(timeout: int = 15) -> str | None: + """Holt den aktuellen Beta-Key vom MakeMKV-Forum. None bei Fehler/kein Treffer.""" + import requests # lazy: haelt extract_key + den Test dependency-frei + antwort = requests.get(FORUM_URL, headers=_HEADERS, timeout=timeout) + antwort.raise_for_status() + return extract_key(antwort.text) + + +def _apply_key(key: str) -> bool: + """Merge den Key in die Settings. save_settings ueberschreibt das GANZE JSON, + darum erst lesen, dann setzen. Rueckgabe True, wenn sich der Key geaendert hat.""" + import db + einstellungen = db.get_settings() + if (einstellungen.get("makemkvAppKey") or "").strip() == key: + return False + einstellungen["makemkvAppKey"] = key + db.save_settings(einstellungen) + return True + + +async def refresh_once() -> bool: + """Einmal holen + anwenden. Best-effort, loggt in die App-Logs. True = geaendert.""" + import db + key = await asyncio.to_thread(fetch_current_key) + if not key: + db.add_log("warning", "makemkv-key", + "Aktueller Beta-Key nicht im Forum gefunden (Format geaendert?).") + return False + geaendert = await asyncio.to_thread(_apply_key, key) + if geaendert: + db.add_log("success", "makemkv-key", + f"MakeMKV-Beta-Key automatisch erneuert (…{key[-6:]}).") + return geaendert + + +async def refresh_loop(intervall_stunden: int = 24, start_verzoegerung_s: int = 60) -> None: + """Periodische Erneuerung (Default: taeglich, damit ein Monatswechsel nie verpasst + wird). Fehler werden geloggt, aber nie geworfen -> die API laeuft weiter.""" + import db + await asyncio.sleep(start_verzoegerung_s) + while True: + try: + await refresh_once() + except Exception as exc: + db.add_log("warning", "makemkv-key", f"Key-Refresh fehlgeschlagen: {exc!r}") + await asyncio.sleep(max(3600, intervall_stunden * 3600)) diff --git a/docker/api/test_makemkv_key.py b/docker/api/test_makemkv_key.py new file mode 100644 index 0000000..aa8f3d9 --- /dev/null +++ b/docker/api/test_makemkv_key.py @@ -0,0 +1,28 @@ +"""Tests fuer den MakeMKV-Beta-Key-Parser (dependency-frei, nur extract_key).""" +from makemkv_key import extract_key + +# Beispiel-Key im echten Format (T- + 64 Zeichen [A-Za-z0-9_]). +_KEY = "T-BSaJ6gwgMx4eIggWkVYXiVP_6zehm7WAO9dEydvzOHFHoZ6YQ82BL5cGpYDxvyRWnS" + + +def test_extract_key_findet_key_im_html(): + html = f'
{_KEY}
' + assert extract_key(html) == _KEY + + +def test_extract_key_ohne_key_gibt_none(): + assert extract_key("kein Schluessel hier") is None + assert extract_key("") is None + + +def test_extract_key_zu_kurz_kein_treffer(): + # Format ist streng T- + genau 64 Zeichen — zu kurz faellt raus. + assert extract_key("T-zukurz123") is None + + +def test_extract_key_liefert_ganzen_key(): + # Regression: die Regex darf den Key NICHT abschneiden (Beta-Keys sind laenger + # als die im Forum genannten 64 Zeichen — hier der volle Key zwischen Text). + treffer = extract_key(f"Vorher-Text {_KEY} Nachher-Text") + assert treffer == _KEY + assert treffer.startswith("T-")