Ampel / ampel (push) Successful in 40s
WAS: rippy/ports.py beschreibt Store/Queue/Bus/Drives als Protocol. Zwei
weitere Doppelungen sind zusammengelegt: db.py (API+Worker) wird
rippy/store, und der Auswurf (api/devices.py + ripping.wirf_disc_aus)
wird rippy/drives/linux. Verhalten unveraendert.
WARUM: Drei Betriebsarten tragen nur, wenn ein Modus die Auswahl der
Treiber hinter vier Nahtstellen ist statt ein eigener Codestand
(KONZEPT-V2.md §1). Diese Etappe zieht die Nahtstellen ein, ohne schon
einen zweiten Treiber zu haben — die kommen in V2-2 (SQLite/LocalQueue)
und V2-4 (Windows).
DIE UNANGENEHMERE DOPPELUNG WAR DER AUSWURF: Er stand zweimal da, mit
UNTERSCHIEDLICHEN Vertraegen — devices.eject wirft OSError, ripping.
wirf_disc_aus gibt False zurueck und wirft nie. Beides ist richtig fuer
seine Seite (Browser-Meldung gegen "ein Rip stirbt nicht an einer
klemmenden Schublade"). Jetzt liegt EINE Mechanik darunter
(auswerfen_mit_grund) und beide Vertraege unveraendert darueber.
Die API-Fassung war ausserdem NIE getestet — jetzt schon, inklusive
"reicht ENOENT/EPERM unveraendert weiter".
get_settings hatte den einzigen echten Verhaltensunterschied der beiden
db.py: die Worker-Fassung schluckte jeden Fehler und gab {} zurueck.
Nicht still entschieden, sondern sichtbar gemacht — der Parameter
bei_fehler_leer steht jetzt in der Signatur, mit der offenen Frage im
Docstring. {} heisst fuer den Aufrufer "nichts gesetzt", nicht "konnte
nicht nachsehen"; das ist dieselbe Klasse wie catch(() => []) im alten
UI. Zu entscheiden in V2-2.
ZWEITER BEINAHE-FEHLER DIESER ETAPPE: linux.py importierte detection
auf Modulebene — und das zieht fcntl. Damit waere ripping.py und ueber
es der NATIVE WINDOWS-WORKER nicht mehr ladbar gewesen. Diesmal haben
die Tests es sofort gefangen (4 Sammelfehler). Behoben an der Wurzel:
Konstanten und die reine classify() leben jetzt in drives/cdrom.py,
ganz ohne fcntl. Nebengewinn — die classify-Tests liefen bisher NUR in
der Ampel ("erst nach dem Push bewiesen") und laufen jetzt ueberall.
Ausserdem: .dockerignore-Testmuster brauchen **, sonst greifen sie nur
in der obersten Ebene. Im laufenden Container nachgezaehlt: 30 test_*.py
lagen in den Images.
GEMESSEN: ruff sauber, 301 Tests gruen + 1 uebersprungen (vorher 290;
+4 neue eject-Tests, +7 classify-Tests die jetzt lokal laufen). Kein
Modul liegt mehr doppelt im Repo.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
88 lines
3.9 KiB
Python
88 lines
3.9 KiB
Python
"""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 läuft zum Monatsende ab. Ohne gültigen Key
|
|
fällt 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 nächsten Rip.
|
|
|
|
Wichtig: Das ist die kostenlose, oeffentliche Beta-LIZENZ der Software selbst —
|
|
NICHT das Entschluesseln oder Verteilen von Disc-Schluesseln. Diese Grenze gilt
|
|
unveraendert; Rippy liefert keine Disc-Schluessel mit und verteilt keine.
|
|
|
|
Richtigstellung 25.07.2026: Hier stand früher, MakeMKV ziehe den AACS-Schluessel
|
|
via LibreDrive ohnehin selbst aus dem Laufwerk. Das stimmt für Blu-ray, aber
|
|
NICHT für 4K-UHD — dort braucht MakeMKV den Volume-Key der jeweiligen Pressung,
|
|
und den bekommt es weder aus dem Laufwerk noch (heute) aus dem Netz. Belege und
|
|
Messungen stehen im Modul-Kopf von makemkv_daten.py.
|
|
|
|
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: hält 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 überschreibt das GANZE JSON,
|
|
darum erst lesen, dann setzen. Rueckgabe True, wenn sich der Key geaendert hat."""
|
|
from rippy import store as 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."""
|
|
from rippy import store as 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 läuft weiter."""
|
|
from rippy import store as 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))
|