From 954f293871f1d233d015f22846182d419ca87bcc Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Sat, 29 Aug 2026 13:52:06 +0200 Subject: [PATCH] =?UTF-8?q?fix(windows):=20Rippen=20ging=20nicht=20?= =?UTF-8?q?=E2=80=94=20drei=20Container-Annahmen=20im=20Rip-Pfad?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Commander: „Das Rippen der disk funktioniert nicht. makemkvcon endete mit Code 11 — letzte Meldung: Das Öffnen der Disk schlug fehl — keine MKV-Datei entstanden" In diesem einen Satz steckten drei Fehler. Alle drei an seinem Laufwerk gemessen, nicht hergeleitet. ## 1. Die Quellenangabe — das war der Abbruch dev:\.\G: MSG:2024 „Unknown device" -> MSG:5010 Öffnen schlug fehl dev:G: 68 TINFO-Zeilen, „Die Aufgabe wurde erfolgreich abgeschlossen" `f"dev:{device_path}"` stand an VIER Stellen. Unter Linux richtig (/dev/sr0), unter Windows nicht: `rippy.drives.windows` meldet den Geraete-Namensraum `\.\G:`, den `CreateFileW` fuer die IOCTLs braucht — MakeMKV kennt ihn nicht. ## 2. „Ö" statt „Ö" `Ö` als UTF-8 (C3 96), gelesen als cp1252. `subprocess` stand auf `text=True` ohne `encoding=`, nahm also die Gebietsschema-Kodierung; Rippy stellt seine Konsole beim Start aber auf UTF-8 (sonst stirbt der Start an einer Umlaut-Logzeile). Direkt gemessen kommen die Bytes je nach Umgebung als UTF-8 ODER cp1252 — deshalb wird jetzt bestimmt statt behauptet. ## 3. Die Ursache fehlte im Fehlertext Gesucht wurde nach ENGLISCHEN Textbausteinen („evaluation period has expired"). Auf einem deutschen Windows trifft keiner. Jetzt zaehlen die Meldungs-NUMMERN, die sprachunabhaengig sind. ⚠️ Beim ersten Anlauf hatte ich 5021 aus dem Kopf mit „Volume-Key unbekannt" beschriftet — falsch. Meine eigene Ausgabe zeigte nur meine Beschriftung statt MakeMKVs Text, sodass es fast durchgegangen waere. Der echte Wortlaut steht jetzt im Code. ## Nebenbefund: der Beta-Key lag zweimal am falschen Ort `makemkv_daten.DATEN_DIR` war fest `/root/.MakeMKV`. Nach der Reparatur des Ordners blieb MSG:5051 trotzdem stehen — weil MakeMKV unter Windows seine Einstellungen in der REGISTRY haelt (HKCU\Software\MakeMKV), nicht in einer settings.conf. Nachgesehen statt vermutet. Und der wichtigste Teil davon: **Ein abgelehnter Schluessel ist schlimmer als gar keiner.** Ohne Key las MakeMKV die Disc noch (68 TINFO), mit dem abgelehnten verweigerte es alles (0 Titel). Deshalb wird jetzt geprueft und bei Ablehnung zurueckgenommen. ## Beweis Echter Rip ueber `ripping.run_makemkv`, kuerzester Titel: Status success, Code 0, „Evangelion 2.22_t03.mkv" 217,9 MB 5036 „Das Kopieren wurde abgeschlossen. 1 Titel wurden gesichert." 828 Tests gruen, ruff sauber. Co-Authored-By: Claude Opus 5 --- docker/api/main.py | 24 +- docker/api/prescan/prescan.py | 9 +- docker/ui/src/pages/Settings.tsx | 17 +- docker/worker/ablauf.py | 29 ++- docker/worker/ripping.py | 43 +++- src/rippy/rip/makemkv_aufruf.py | 315 +++++++++++++++++++++++++++ src/rippy/rip/makemkv_daten.py | 63 +++++- src/rippy/rip/test_makemkv_aufruf.py | 215 ++++++++++++++++++ src/rippy/setup_fenster.py | 17 +- 9 files changed, 693 insertions(+), 39 deletions(-) create mode 100644 src/rippy/rip/makemkv_aufruf.py create mode 100644 src/rippy/rip/test_makemkv_aufruf.py diff --git a/docker/api/main.py b/docker/api/main.py index c20ec6b..d962dc5 100644 --- a/docker/api/main.py +++ b/docker/api/main.py @@ -2445,14 +2445,30 @@ async def makemkv_key_holen(): detail="Im Forum-Beitrag stand kein Key im erwarteten Format. " "Vermutlich hat sich die Seite geändert.") geaendert = await asyncio.to_thread(key_modul._apply_key, key) + + # Nimmt MakeMKV ihn an? Nur MakeMKV selbst weiss das. + # + # Am 29.08.2026 auf dem Rechner des Commanders: der frische Schluessel + # wurde mit MSG 5020 („ungueltig") und 5021 („Programmversion zu alt") + # abgelehnt — sein MakeMKV 1.18.4 ist aelter als der Schluessel verlangt. + # Ohne diese Rueckfrage haette hier „erfolgreich geholt" gestanden, und + # der naechste Rip waere trotzdem gescheitert. + from rippy.rip import makemkv_aufruf + from rippy.tools import katalog as werkzeug_katalog + + urteil = await asyncio.to_thread( + makemkv_aufruf.key_setzen_und_pruefen, key, + werkzeug_katalog.finden("makemkv")) await asyncio.to_thread( - db.add_log, "success", "makemkv-key", - "Beta-Key aus dem Forum geholt (…%s)%s" - % (key[-6:], "" if geaendert else " — war schon der aktuelle")) + db.add_log, "success" if urteil["ok"] else "warning", "makemkv-key", + "Beta-Key aus dem Forum geholt (…%s)%s%s" + % (key[-6:], "" if geaendert else " — war schon der aktuelle", + "" if urteil["ok"] else " — ABGELEHNT: " + urteil["grund"])) # Der Key selbst wird NICHT zurueckgegeben: Er steht in den Einstellungen, # und ein zweiter Weg zu demselben Wert ist ein zweiter Weg, ihn zu # verlieren. - return {"geholt": True, "geaendert": geaendert, "endet_auf": key[-6:]} + return {"geholt": True, "geaendert": geaendert, "endet_auf": key[-6:], + "angenommen": urteil["ok"], "grund": urteil["grund"]} @app.post("/system/keydb") diff --git a/docker/api/prescan/prescan.py b/docker/api/prescan/prescan.py index eea8cf4..bde4998 100644 --- a/docker/api/prescan/prescan.py +++ b/docker/api/prescan/prescan.py @@ -10,6 +10,8 @@ import subprocess from typing import Dict, List, Optional from rippy import drives as _laufwerks_schicht +from rippy.rip.makemkv_aufruf import quelle as makemkv_quelle +from rippy.rip.makemkv_aufruf import text_von from rippy.tools import katalog as werkzeug_katalog from clients.tmdb import TMDBClient from clients.jikan import JikanClient @@ -365,12 +367,11 @@ class PreScan: makemkv = werkzeug_katalog.finden("makemkv") if makemkv: result = subprocess.run( - [makemkv, "-r", "--noscan", "--minlength=300", "info", f"dev:{device_path}"], + [makemkv, "-r", "--noscan", "--minlength=300", "info", makemkv_quelle(device_path)], capture_output=True, - text=True, - timeout=120 + timeout=120 # binaer lesen — Begruendung in text_von ) - for line in result.stdout.split('\n'): + for line in text_von(result.stdout).split('\n'): if line.startswith('TINFO:'): parts = line.split(',') if len(parts) >= 5: diff --git a/docker/ui/src/pages/Settings.tsx b/docker/ui/src/pages/Settings.tsx index 479c958..5b89462 100644 --- a/docker/ui/src/pages/Settings.tsx +++ b/docker/ui/src/pages/Settings.tsx @@ -270,10 +270,19 @@ export default function SettingsPage() { const r = await api.post('/system/makemkv-key/holen') const antwort = await api.get('/settings') setSettings({ ...defaultSettings, ...antwort.data }) - setKeyMeldung(r.data?.geaendert - ? `Neuer Key geholt (endet auf …${r.data.endet_auf}).` - : `Schon aktuell (endet auf …${r.data?.endet_auf || '?'}).`) - toast('success', 'MakeMKV-Beta-Key aus dem Forum geholt.') + if (r.data?.angenommen === false) { + // Geholt ist nicht angenommen. Am 29.08.2026 lehnte MakeMKV 1.18.4 + // den aktuellen Key ab („Programmversion zu alt") — ohne diesen + // Hinweis stünde hier ein Erfolg, und der nächste Rip scheiterte + // trotzdem. + setKeyMeldung('Fehlgeschlagen: ' + (r.data.grund || 'MakeMKV nimmt den Key nicht an.')) + toast('error', r.data.grund || 'MakeMKV nimmt den Key nicht an.') + } else { + setKeyMeldung(r.data?.geaendert + ? `Neuer Key geholt und angenommen (endet auf …${r.data.endet_auf}).` + : `Schon aktuell (endet auf …${r.data?.endet_auf || '?'}).`) + toast('success', 'MakeMKV-Beta-Key aus dem Forum geholt.') + } } catch (e: any) { const grund = e?.response?.data?.detail || 'Das Forum ist nicht erreichbar.' diff --git a/docker/worker/ablauf.py b/docker/worker/ablauf.py index 44ef108..4447218 100644 --- a/docker/worker/ablauf.py +++ b/docker/worker/ablauf.py @@ -47,7 +47,7 @@ import time import requests from rippy import store as db -from rippy.rip import makemkv_daten +from rippy.rip import makemkv_aufruf, makemkv_daten import medien from rippy.core import notify @@ -243,17 +243,24 @@ def _makemkv_key_anwenden(einstellungen: dict) -> None: key = (einstellungen.get("makemkvAppKey") or "").strip() if not key: return - pfad = os.path.join(makemkv_daten.DATEN_DIR, "settings.conf") try: - os.makedirs(makemkv_daten.DATEN_DIR, exist_ok=True) - try: - with open(pfad, encoding="utf-8", errors="replace") as f: - alt = f.read() - except OSError: - alt = "" - with open(pfad, "w", encoding="utf-8", newline="\n") as f: - f.write(makemkv_daten.settings_conf_zusammenfuehren(alt, key)) - except OSError as e: + # WOHIN, entscheidet der Rechner — unter Windows die Registry, sonst + # die settings.conf. Begruendung und Messung in + # `rippy.rip.makemkv_aufruf.key_ablegen`. + # + # UND nachsehen, ob MakeMKV ihn annimmt: Ein abgelehnter Schluessel + # ist schlimmer als gar keiner — ohne las MakeMKV die Disc noch, mit + # einem abgelehnten verweigerte es alles (gemessen 29.08.2026). Die + # paar Sekunden Rueckfrage sind nichts gegen einen Rip, der Stunden + # laeuft und sonst am ersten Titel scheitert. + from ripping import werkzeug + + urteil = makemkv_aufruf.key_setzen_und_pruefen(key, werkzeug("makemkv")) + if not urteil["ok"]: + db.add_log("warning", "worker", + "MakeMKV nimmt den hinterlegten Beta-Key nicht an: %s" + % urteil["grund"]) + except (OSError, ImportError) as e: db.add_log("warning", "worker", f"MakeMKV-Key konnte nicht gesetzt werden: {e}") diff --git a/docker/worker/ripping.py b/docker/worker/ripping.py index de980de..5b89b91 100644 --- a/docker/worker/ripping.py +++ b/docker/worker/ripping.py @@ -37,6 +37,8 @@ from rippy.drives.linux import ( # noqa: F401 ) from rippy.drives.linux import auswerfen_versuchen as wirf_disc_aus # noqa: F401 from rippy.platform.winlauf import OHNE_FENSTER +from rippy.rip.makemkv_aufruf import KRITISCHE_CODES, text_von +from rippy.rip.makemkv_aufruf import quelle as makemkv_quelle from rippy.tools import katalog as werkzeuge _NACKTER_NAME = {"makemkv": "makemkvcon", "handbrake": "HandBrakeCLI"} @@ -102,7 +104,7 @@ def build_makemkv_cmd(device_path: str, output_dir: str, titel: str = "all") -> "--noscan", "--progress=-same", "mkv", - f"dev:{device_path}", + makemkv_quelle(device_path), str(titel), output_dir, ] @@ -257,11 +259,11 @@ def lies_titel_info(device_path: str, timeout: int = 300) -> tuple: wäre eine Minute Wartezeit für nichts. """ ergebnis = subprocess.run( - [werkzeug("makemkv"), "-r", "--noscan", "info", f"dev:{device_path}"], - capture_output=True, text=True, timeout=timeout, + [werkzeug("makemkv"), "-r", "--noscan", "info", makemkv_quelle(device_path)], + capture_output=True, timeout=timeout, # binaer: siehe text_von creationflags=OHNE_FENSTER, ) - ausgabe = ergebnis.stdout or "" + ausgabe = text_von(ergebnis.stdout) return parse_titel_info(ausgabe), parse_stream_info(ausgabe) @@ -330,13 +332,13 @@ def lies_titel_dauern(device_path: str, timeout: int = 300) -> dict: """Fragt die Titel-Laufzeiten der Disc ab (makemkvcon info, Robot-Mode).""" try: ergebnis = subprocess.run( - [werkzeug("makemkv"), "-r", "--noscan", "info", f"dev:{device_path}"], - capture_output=True, text=True, timeout=timeout, + [werkzeug("makemkv"), "-r", "--noscan", "info", makemkv_quelle(device_path)], + capture_output=True, timeout=timeout, # binaer: siehe text_von creationflags=OHNE_FENSTER, ) except (OSError, subprocess.TimeoutExpired): return {} - return parse_tinfo_dauern(ergebnis.stdout or "") + return parse_tinfo_dauern(text_von(ergebnis.stdout)) def parse_scan_dauer(ausgabe: str) -> int: @@ -730,8 +732,17 @@ def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str build_makemkv_cmd(device_path, output_dir, titel), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, - text=True, - bufsize=1, + # BINAER lesen und selbst dekodieren (siehe `text_von`): + # `text=True` nahm die Gebietsschema-Kodierung, makemkvcon + # schrieb aber UTF-8 — daraus wurde beim Commander + # „Das Öffnen der Disk schlug fehl". + # + # bufsize=0 statt 1: Zeilenpufferung gibt es im Binaermodus + # nicht (Python warnt und puffert doch). Ungepuffert kommt + # jede Fortschrittszeile sofort an — bei einem Rip, der + # Stunden laeuft, ist das der Unterschied zwischen einem + # Balken und einem eingefrorenen Fenster. + bufsize=0, creationflags=OHNE_FENSTER, ) @@ -741,6 +752,12 @@ def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str # abgelaufen) steht Zeilen davor und ging im Fehlertext verloren # (Befund 24.07., Summer-Wars-UHD). kritische_meldungen = [] + # Die Textmuster bleiben als Rueckfall fuer englische Installationen — + # die MELDUNGSNUMMERN sind aber die verlaessliche Quelle. Beim + # Commander meldet MakeMKV deutsch, also traf hier am 29.08.2026 kein + # einziges Muster: Im Fehler stand nur „Das Öffnen der Disk schlug + # fehl", waehrend die Ursache eine Zeile darueber („Der Testzeitraum + # ist abgelaufen", MSG 5051) unter den Tisch fiel. KRITISCH = ( "volume key is unknown", "evaluation period has expired", @@ -748,7 +765,8 @@ def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str "Too old version", ) try: - for line in process.stdout: + for rohzeile in process.stdout: + line = text_von(rohzeile) progress = get_progress_from_prgv(line) if progress >= 0: if progress_cb: @@ -758,7 +776,10 @@ def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str if meldung is None: continue code, letzte_meldung = meldung - if any(muster in letzte_meldung for muster in KRITISCH): + if code in KRITISCHE_CODES: + kritische_meldungen.append( + "%s (%s)" % (KRITISCHE_CODES[code], letzte_meldung.strip())) + elif any(muster in letzte_meldung for muster in KRITISCH): kritische_meldungen.append(letzte_meldung) if log_cb: try: diff --git a/src/rippy/rip/makemkv_aufruf.py b/src/rippy/rip/makemkv_aufruf.py new file mode 100644 index 0000000..b291783 --- /dev/null +++ b/src/rippy/rip/makemkv_aufruf.py @@ -0,0 +1,315 @@ +"""Wie makemkvcon angesprochen und wie seine Antwort gelesen wird. + +## Warum das im GEMEINSAMEN Paket liegt + +Beide Seiten rufen makemkvcon auf: der Worker zum Rippen +(`docker/worker/ripping.py`) und die API beim Vor-Scan +(`docker/api/prescan/prescan.py`). Sie stecken in verschiedenen Images und +können einander nicht importieren — deshalb stand die Formatzeile zweimal da, +und beide Male falsch. + +Genau das ist das Muster, das in diesem Projekt schon dreimal Geld gekostet +hat: `os.path` (viermal repariert, jetzt `rippy/pfade.py`), `/app/media` und +`/root/.MakeMKV`. Eine Entscheidung, zwei Orte — einer altert. +""" + +import sys + + +def quelle(device_path: str, plattform_name: str = None) -> str: + """Wie MakeMKV DIESES Laufwerk nennt (pure Funktion, testbar). + + ## Der Befund des Commanders (29.08.2026) + + > „Das Rippen der disk funktioniert nicht. makemkvcon endete mit Code 11 — + > letzte Meldung: Das Öffnen der Disk schlug fehl — keine MKV-Datei + > entstanden" + + An seinem Laufwerk gemessen, mit `info` (öffnet die Disc genauso wie + `mkv`, rippt aber nichts): + + dev:\\\\.\\G: MSG:2024 „Unknown device" -> MSG:5010 Öffnen schlug fehl + dev:G: 68 TINFO-Zeilen, „Die Aufgabe wurde erfolgreich + abgeschlossen" + + Vorher stand überall `f"dev:{device_path}"`. Unter Linux ist das richtig: + dort heißt das Laufwerk `/dev/sr0`, und genau so will MakeMKV es haben. + Unter Windows meldet `rippy.drives.windows` den Geräte-Namensraum + `\\\\.\\G:` — den braucht `CreateFileW` für die IOCTLs, aber MakeMKV kennt + ihn nicht. Es will den Laufwerksbuchstaben. + + `plattform_name` ist einspritzbar, damit beide Fälle überall prüfbar sind. + Entschieden wird aber nach der FORM des Pfades, nicht nach dem Rechner — + dieselbe Regel wie in `rippy.pfade`: Ein Windows-Pfad bleibt ein + Windows-Pfad, auch wenn gerade Linux danach fragt. + """ + pfad = (device_path or "").strip() + if not pfad: + return "" + # Geraete-Namensraum abstreifen: \\.\G: -> G: (auch \\?\ kommt vor) + for vorsatz in ("\\\\.\\", "\\\\?\\"): + if pfad.startswith(vorsatz): + pfad = pfad[len(vorsatz):] + break + # „G:\" -> „G:" — mit Trenner geht es zwar auch (gemessen), aber ohne ist + # es das, was MakeMKV in seiner eigenen Laufwerksliste ausgibt. + if len(pfad) == 3 and pfad[1] == ":" and pfad[2] in "\\/": + pfad = pfad[:2] + return "dev:%s" % pfad + + +def text_von(roh) -> str: + """makemkvcon-Ausgabe lesbar machen — egal in welcher Kodierung sie kommt. + + ## Der Befund des Commanders (29.08.2026) + + > „letzte Meldung: Das Öffnen der Disk schlug fehl" + + `Ö` ist `Ö` als UTF-8 (Bytes C3 96), gelesen als cp1252. Die Ausgabe kam + also in einer anderen Kodierung an, als sie gelesen wurde. + + Beide Seiten waren für sich richtig: `subprocess` stand auf `text=True` + **ohne** `encoding=`, benutzt also die Gebietsschema-Kodierung — auf einem + deutschen Windows cp1252. Und Rippy stellt seine Konsole beim Start + ausdrücklich auf UTF-8 (`daemon.ausgabe_auf_utf8_stellen`, sonst stirbt + der Start an einer Umlaut-Logzeile). Ein Konsolenprogramm richtet sich + danach — makemkvcon schrieb UTF-8 in eine Leitung, an deren Ende cp1252 + erwartet wurde. + + Direkt an makemkvcon gemessen, ohne unsere Konsolen-Umstellung: die Bytes + sind dann **cp1252** (`Das Öffnen` liest sich nur so richtig). Die + Kodierung hängt also wirklich an der Umgebung und ist nicht festzulegen. + + Deshalb wird sie nicht behauptet, sondern bestimmt: UTF-8 streng + versuchen, sonst cp1252. Streng ist hier der Punkt — mit `errors=replace` + ginge jeder Umlaut still in einem Fragezeichen unter, und wir würden es + nie erfahren. + """ + if isinstance(roh, str): + return roh + if not roh: + return "" + try: + return roh.decode("utf-8") + except UnicodeDecodeError: + # cp1252 kann jedes Byte abbilden -> hier kann nichts mehr scheitern. + return roh.decode("cp1252", "replace") + + +#: Meldungs-Nummern, die die URSACHE eines Fehlschlags benennen. Sie sind +#: sprachunabhängig — anders als die Texte, auf die vorher geprüft wurde. +#: +#: Bis zum 29.08.2026 suchte `ripping.py` nur englische Textbausteine +#: („evaluation period has expired", „volume key is unknown"). Auf dem +#: Commander-PC meldet MakeMKV deutsch, also traf keiner: Im Fehler stand nur +#: „Das Öffnen der Disk schlug fehl" — die Ursache eine Zeile darüber fiel +#: unter den Tisch. +#: +#: ⚠️ Jede Nummer hier ist an seinem Laufwerk GESEHEN worden, im Wortlaut. +#: Beim ersten Anlauf hatte ich 5021 aus dem Kopf mit „Volume-Key unbekannt" +#: beschriftet — falsch, und meine eigene Ausgabe zeigte nur meine +#: Beschriftung statt MakeMKVs Text, sodass es fast durchgegangen wäre. +#: Genau der Fehler, vor dem AGENTS Regel D warnt. Der echte Wortlaut: +#: +#: 2024 „Unknown device - '\\\\.\\G:'" +#: 5020 „Der hinterlegte Aktivierungsschlüssel ist ungültig." +#: 5021 „Diese Programmversion ist zu alt. Bitte laden Sie die aktuelle +#: Version von http://www.makemkv.com/ herunter." +#: 5051 „Der Testzeitraum ist abgelaufen. Erwerben Sie bitte einen +#: Aktivierungsschlüssel …" +KRITISCHE_CODES = { + 2024: "MakeMKV kennt dieses Laufwerk nicht", + 5020: "Der hinterlegte MakeMKV-Schlüssel wird nicht angenommen", + 5021: "MakeMKV ist zu alt für den aktuellen Beta-Schlüssel — bitte MakeMKV " + "aktualisieren", + 5051: "MakeMKV ist nicht freigeschaltet (Testzeitraum abgelaufen)", +} + +#: Meldungen, die sagen: DIESER Schlüssel taugt für DIESE Fassung nicht. +KEY_ABGELEHNT = (5020, 5021) + + +def plattform(name: str = None) -> str: + """Nur für Tests, die den Rechner ausdrücklich benennen wollen.""" + return (name if name is not None else sys.platform).lower() + + +#: Wo MakeMKV unter Windows SEINE Einstellungen hält — gemessen, nicht geraten. +REGISTRY_PFAD = r"Software\MakeMKV" +KEY_NAME = "app_Key" + + +def key_ablegen(key: str, plattform_name: str = None, schreiber=None, + datei_schreiber=None) -> str: + """Den Beta-Key dorthin legen, wo makemkvcon ihn WIRKLICH sucht. + + Gibt zurück, wohin geschrieben wurde (für die Protokollzeile). + + ## Der Befund des Commanders (29.08.2026) + + Beim ersten echten Rip-Versuch meldete makemkvcon: + + MSG:5051 „Der Testzeitraum ist abgelaufen. Erwerben Sie bitte einen + Aktivierungsschlüssel" + + — obwohl in Rippys Einstellungen ein aktueller Beta-Key lag. Zwei Fehler + hintereinander, beide gemessen: + + **1. Falscher Ordner.** `makemkv_daten.DATEN_DIR` war fest + `/root/.MakeMKV`, ein Container-Pfad. Repariert (siehe dort). + + **2. Falsche ART von Ablage.** Auch mit richtigem Ordner blieb die Meldung + stehen. Nachgesehen, was auf seinem Rechner wirklich existiert: + + C:\\Users\\TobisPC\\.MakeMKV\\ nur _private_data.tar (6,4 MB) + HKCU\\Software\\MakeMKV app_UpdateLastCheck, app_SiteInfoString, + app_BackupDecrypted, path_BackupDirMRU … + + **Unter Windows hält MakeMKV seine Einstellungen in der Registry.** Die + `settings.conf` ist der Linux-Weg; im Container stimmt sie, hier nicht. Der + Ordner `.MakeMKV` existiert trotzdem — aber nur für den Schlüsselspeicher. + + Rippy hat den Key also zweimal brav gespeichert und beide Male an einen + Ort, den niemand liest. Eine Automatik, die ins Leere schreibt, ist + schlimmer als keine: Sie sieht aus wie erledigt. + + ## Warum hier NUR ein einziger Wert geschrieben wird + + Weil in dieser Registry die Arbeit des Commanders steckt — + Laufwerksprofile, Fensterlage, zuletzt benutzte Ordner. Es wird genau + `app_Key` gesetzt und sonst nichts angefasst; gelöscht wird nie. (Am + 28.08.2026 hat eine zu großzügige Testsuite schon einmal echte + Registry-Einträge von ihm entfernt.) + """ + key = (key or "").strip() + if not key: + return "" + if not plattform(plattform_name).startswith("win"): + # Linux/Container: die settings.conf, wie gehabt. + return (datei_schreiber or _in_datei)(key) + return (schreiber or _in_registry)(key) + + +def _in_registry(key: str) -> str: + import winreg + + with winreg.CreateKey(winreg.HKEY_CURRENT_USER, REGISTRY_PFAD) as schluessel: + winreg.SetValueEx(schluessel, KEY_NAME, 0, winreg.REG_SZ, key) + return "HKCU\\" + REGISTRY_PFAD + + +def key_zuruecknehmen(plattform_name: str = None, entferner=None) -> bool: + """Einen abgelehnten Schlüssel wieder entfernen. True, wenn etwas wegging.""" + if not plattform(plattform_name).startswith("win"): + return False + return (entferner or _aus_registry)() + + +def _aus_registry() -> bool: + import winreg + + try: + with winreg.OpenKey(winreg.HKEY_CURRENT_USER, REGISTRY_PFAD, 0, + winreg.KEY_ALL_ACCESS) as schluessel: + winreg.DeleteValue(schluessel, KEY_NAME) + return True + except OSError: + return False # war nicht da — auch gut + + +def key_pruefen(programm: str, laufen=None) -> dict: + """Nimmt MakeMKV den hinterlegten Schlüssel an? — es selbst gefragt. + + `{"ok": bool, "code": int|None, "grund": str}`. + + ## Warum GEFRAGT und nicht gerechnet (Befund 29.08.2026) + + Rippy hat den frischen Beta-Key aus dem Forum geholt und abgelegt — alles + richtig. Auf dem Rechner des Commanders antwortete MakeMKV darauf: + + 5020 „Der hinterlegte Aktivierungsschlüssel ist ungültig." + 5021 „Diese Programmversion ist zu alt." + + Sein MakeMKV 1.18.4 ist älter als der Schlüssel verlangt. Rippys + Update-Prüfung kann das NICHT sehen: makemkv.com antwortet seit Tagen mit + HTTP 525, und die Ausweichquellen kennen als höchste Fassung genau 1.18.4 + (gemessen). Die einzige Instanz, die die Wahrheit kennt, ist MakeMKV + selbst. + + ## Warum das wichtiger ist, als es klingt + + **Ein abgelehnter Schlüssel ist schlimmer als gar keiner.** Ohne Schlüssel + lief MakeMKV im abgelaufenen Testmodus und las die Disc immerhin noch (68 + TINFO-Zeilen). Mit dem abgelehnten verweigerte es alles — 0 Titel, kein + Laufwerk. Wer also blind schreibt, verschlechtert den Zustand. + + Der Aufruf ist billig: kein Datenträger nötig, MakeMKV meldet die + Freischaltung schon beim Start. + """ + if not programm: + return {"ok": False, "code": None, "grund": "MakeMKV nicht gefunden"} + laufen = laufen or _makemkvcon_laufen + try: + ausgabe = laufen(programm) + except Exception as e: # noqa: BLE001 + return {"ok": False, "code": None, + "grund": "MakeMKV nicht befragbar (%s)" % type(e).__name__} + + for zeile in text_von(ausgabe).splitlines(): + if not zeile.startswith("MSG:"): + continue + try: + code = int(zeile[4:].split(",", 1)[0]) + except ValueError: + continue + if code in KEY_ABGELEHNT: + return {"ok": False, "code": code, "grund": KRITISCHE_CODES[code]} + return {"ok": True, "code": None, "grund": ""} + + +def _makemkvcon_laufen(programm: str): + import subprocess + + from rippy.platform.winlauf import OHNE_FENSTER + + # `info disc:9999` fragt nur die Laufwerksliste ab — kein Datenträger + # nötig, und die Freischaltungs-Meldung kommt trotzdem. + ergebnis = subprocess.run([programm, "-r", "--noscan", "info", "disc:9999"], + capture_output=True, timeout=120, + creationflags=OHNE_FENSTER) + return ergebnis.stdout + + +def key_setzen_und_pruefen(key: str, programm: str, plattform_name: str = None, + schreiber=None, pruefer=None, entferner=None) -> dict: + """Schlüssel ablegen — und wieder wegnehmen, wenn MakeMKV ihn ablehnt. + + Gibt `{"ok", "wohin", "grund"}` zurück. Siehe `key_pruefen`, warum das + Zurücknehmen wichtig ist. + """ + wohin = key_ablegen(key, plattform_name, schreiber=schreiber) + if not wohin: + return {"ok": False, "wohin": "", "grund": "kein Schlüssel angegeben"} + urteil = (pruefer or key_pruefen)(programm) + if urteil["ok"]: + return {"ok": True, "wohin": wohin, "grund": ""} + key_zuruecknehmen(plattform_name, entferner=entferner) + return {"ok": False, "wohin": wohin, "grund": urteil["grund"]} + + +def _in_datei(key: str) -> str: + import os + + from rippy.rip import makemkv_daten + + ordner = makemkv_daten.DATEN_DIR + pfad = os.path.join(ordner, "settings.conf") + os.makedirs(ordner, exist_ok=True) + try: + with open(pfad, encoding="utf-8", errors="replace") as f: + alt = f.read() + except OSError: + alt = "" + with open(pfad, "w", encoding="utf-8", newline="\n") as f: + f.write(makemkv_daten.settings_conf_zusammenfuehren(alt, key)) + return pfad diff --git a/src/rippy/rip/makemkv_daten.py b/src/rippy/rip/makemkv_daten.py index c91f3e2..800c39c 100644 --- a/src/rippy/rip/makemkv_daten.py +++ b/src/rippy/rip/makemkv_daten.py @@ -62,13 +62,68 @@ es eins: `rippy`. Die Kopie ist weg, der Wächter-Test damit gegenstandslos. import io import os import re +import sys import tarfile from datetime import datetime, timezone -# Container-Pfad aus der Umgebung: /root/.MakeMKV im Worker (nur dort sucht -# makemkvcon), /app/makemkv-data in der API. Beide zeigen laut -# docker-compose.yml auf DASSELBE Host-Verzeichnis. -DATEN_DIR = os.getenv("MAKEMKV_DATA_DIR", "/root/.MakeMKV") +def daten_verzeichnis(umgebung=None, plattform_name=None, existiert=None) -> str: + """Wo makemkvcon SEINE Einstellungen sucht — je Rechner ein anderer Ort. + + ## Der Befund des Commanders (29.08.2026) + + Beim ersten echten Rip-Versuch meldete makemkvcon neben dem Öffnen-Fehler + auch das hier (an seiner Disc gemessen): + + MSG:5051 „Der Testzeitraum ist abgelaufen. Erwerben Sie bitte einen + Aktivierungsschlüssel" + + Und das, obwohl in seinen Einstellungen ein gültiger Beta-Key lag. + + Hier stand `os.getenv("MAKEMKV_DATA_DIR", "/root/.MakeMKV")`. Im Container + stimmt das. Auf einem Windows-PC schrieb Rippy den Key damit nach + `/root/.MakeMKV` — was dort ein Ordner namens `root\\.MakeMKV` auf dem + aktuellen Laufwerk ist. MakeMKV liest `%USERPROFILE%\\.MakeMKV` + (gemessen am 26.07.2026 auf genau diesem Rechner, MakeMKV 1.18.4), und + dort lag **keine settings.conf**. + + Der Key wurde also brav geholt, brav gespeichert — nur an einer Stelle, + die niemand liest. Eine Automatik, die ins Leere schreibt, ist schlimmer + als keine: Sie sieht aus wie erledigt. + + `docker/worker/schluessel.py` kannte den richtigen Ort seit einem Monat. + Zwei Module, zwei Antworten, eine davon falsch — dieselbe Sorte Fund wie + `os.path` und `/app/media`. + + Alles einspritzbar, damit jeder Betriebsfall ohne ihn prüfbar ist. + """ + umgebung = os.environ if umgebung is None else umgebung + existiert = existiert or os.path.isdir + gesetzt = (umgebung.get("MAKEMKV_DATA_DIR") or "").strip() + if gesetzt: + return gesetzt # docker-compose entscheidet, wenn es das tut + + system = (plattform_name if plattform_name is not None else sys.platform).lower() + if not system.startswith("win"): + return "/root/.MakeMKV" + + # Windows: MakeMKV ist über die Versionen umgezogen. Geprüft wird, welcher + # Ort wirklich da ist, statt einen zu behaupten. Der erste ist zugleich + # der Ort zum ANLEGEN, wenn es noch gar keinen gibt — MakeMKV legt sein + # Verzeichnis erst beim ersten Speichern an. + profil = umgebung.get("USERPROFILE") or os.path.expanduser("~") + kandidaten = [ + os.path.join(profil, ".MakeMKV"), + os.path.join(umgebung.get("APPDATA") or "", "MakeMKV"), + os.path.join(umgebung.get("LOCALAPPDATA") or "", "MakeMKV"), + ] + for pfad in kandidaten: + if pfad and existiert(pfad): + return pfad + return kandidaten[0] + + +#: Beim Laden ausgerechnet — die Aufrufer kennen ihn als Konstante. +DATEN_DIR = daten_verzeichnis() # GROSS geschrieben — unter Linux case-sensitiv, "keydb.cfg" wird ignoriert. KEYDB_NAME = "KEYDB.cfg" diff --git a/src/rippy/rip/test_makemkv_aufruf.py b/src/rippy/rip/test_makemkv_aufruf.py new file mode 100644 index 0000000..304649b --- /dev/null +++ b/src/rippy/rip/test_makemkv_aufruf.py @@ -0,0 +1,215 @@ +"""Wie makemkvcon angesprochen und wie seine Antwort gelesen wird. + +## Der Befund des Commanders (29.08.2026) + +> „Das Rippen der disk funktioniert nicht. makemkvcon endete mit Code 11 — +> letzte Meldung: Das Öffnen der Disk schlug fehl — keine MKV-Datei +> entstanden" + +In dem einen Satz stecken DREI Fehler, und alle drei sind an seinem Laufwerk +gemessen worden: + +**1. Die Quellenangabe.** Rippy uebergab `dev:\\\\.\\G:`. + + dev:\\\\.\\G: MSG:2024 „Unknown device" -> MSG:5010 Öffnen schlug fehl + dev:G: 68 TINFO-Zeilen, „Die Aufgabe wurde erfolgreich abgeschlossen" + +**2. `Ö` statt `Ö`.** Das ist `Ö` als UTF-8 (C3 96), gelesen als cp1252 — +also eine Ausgabe, die anders geschrieben als gelesen wurde. + +**3. Die Ursache fehlte.** Im selben Lauf meldete makemkvcon MSG:5051 „Der +Testzeitraum ist abgelaufen". Gesucht wurde aber nur nach dem ENGLISCHEN +„evaluation period has expired" — auf einem deutschen Windows trifft das nie. +Uebrig blieb die nichtssagende letzte Zeile. +""" + +import pytest + +from rippy.rip import makemkv_aufruf as auf + + +# ── 1. Die Quellenangabe ──────────────────────────────────────────────── +@pytest.mark.parametrize("geraet,erwartet", [ + # DER Fall des Commanders: so meldet rippy.drives.windows das Laufwerk. + (r"\\.\G:", "dev:G:"), + (r"\\?\G:", "dev:G:"), + ("G:", "dev:G:"), + ("G:\\", "dev:G:"), + ("G:/", "dev:G:"), + # Linux bleibt unveraendert — die Reparatur darf den Container nicht anfassen. + ("/dev/sr0", "dev:/dev/sr0"), + ("/dev/sr1", "dev:/dev/sr1"), +]) +def test_jede_schreibweise_wird_zu_dem_was_makemkv_versteht(geraet, erwartet): + assert auf.quelle(geraet) == erwartet + + +def test_ohne_geraet_kommt_nichts(): + """Ein `dev:` ohne Pfad waere eine Quellenangabe, die nichts benennt.""" + assert auf.quelle("") == "" + assert auf.quelle(None) == "" + assert auf.quelle(" ") == "" + + +def test_ein_langer_windows_pfad_wird_nicht_verstuemmelt(): + """Die Kuerzung gilt NUR fuer die nackte Laufwerkswurzel.""" + assert auf.quelle(r"\\.\D:\Ordner") == "dev:D:\\Ordner" + + +# ── 2. Die Kodierung ──────────────────────────────────────────────────── +def test_utf8_ausgabe_wird_richtig_gelesen(): + """So kommt es an, wenn Rippy die Konsole auf UTF-8 gestellt hat.""" + assert auf.text_von("Das Öffnen".encode("utf-8")) == "Das Öffnen" + + +def test_cp1252_ausgabe_wird_AUCH_richtig_gelesen(): + """So kommt es an, wenn nicht — direkt an makemkvcon gemessen.""" + assert auf.text_von("Das Öffnen".encode("cp1252")) == "Das Öffnen" + + +def test_der_fehler_des_commanders_kommt_NICHT_zurueck(): + """`Ö` darf nirgends mehr entstehen.""" + for kodierung in ("utf-8", "cp1252"): + gelesen = auf.text_von("Das Öffnen der Disk schlug fehl".encode(kodierung)) + assert "Ã" not in gelesen, "Doppelt kodiert: %r" % gelesen + assert gelesen == "Das Öffnen der Disk schlug fehl" + + +def test_text_bleibt_text_und_leer_bleibt_leer(): + assert auf.text_von("schon Text") == "schon Text" + assert auf.text_von(b"") == "" + assert auf.text_von(None) == "" + + +def test_unlesbare_bytes_werfen_nicht(): + """Eine kaputte Zeile darf keinen laufenden Rip umwerfen.""" + assert auf.text_von(b"\xff\xfe kaputt") is not None + + +# ── 3. Die Ursache ────────────────────────────────────────────────────── +def test_die_gemessenen_ursachen_haben_eine_nummer(): + """Nummern gelten in jeder Sprache, Texte nicht.""" + assert 2024 in auf.KRITISCHE_CODES, "Unknown device" + assert 5051 in auf.KRITISCHE_CODES, "Testzeitraum abgelaufen" + assert 5020 in auf.KRITISCHE_CODES, "Aktivierungsschluessel ungueltig" + assert 5021 in auf.KRITISCHE_CODES, "Programmversion zu alt" + + +def test_jede_ursache_ist_auf_deutsch_erklaert(): + """Der Commander liest diese Zeile — eine Nummer allein hilft ihm nicht.""" + for code, text in auf.KRITISCHE_CODES.items(): + assert len(text) > 20, "%d ist nicht erklaert: %r" % (code, text) + assert not text.startswith("MSG"), code + + +# ── 4. Wohin gehoert der Schluessel — und was, wenn er abgelehnt wird ──── +# +# Weiterer Befund vom 29.08.2026, in zwei Schritten gemessen: +# +# Erst schrieb Rippy den Beta-Key nach `/root/.MakeMKV/settings.conf` — ein +# Container-Pfad. Nach der Reparatur des Ordners blieb MSG:5051 +# („Testzeitraum abgelaufen") trotzdem stehen. Nachgesehen, was auf dem +# Rechner wirklich existiert: +# +# C:\Users\TobisPC\.MakeMKV\ nur _private_data.tar (6,4 MB) +# HKCU\Software\MakeMKV app_UpdateLastCheck, app_SiteInfoString, … +# +# Unter Windows haelt MakeMKV seine Einstellungen in der REGISTRY. Rippy hat +# den Key also zweimal brav gespeichert — beide Male an einen Ort, den +# niemand liest. +# +# Und dann der Nachschlag: Mit dem Key in der Registry verschwand 5051, dafuer +# kam 5020 („Aktivierungsschluessel ungueltig") und 5021 („Diese +# Programmversion ist zu alt"). Sein MakeMKV 1.18.4 ist aelter als der +# aktuelle Beta-Key verlangt. WICHTIG: Ohne Key las MakeMKV die Disc noch (68 +# TINFO-Zeilen), mit dem abgelehnten verweigerte es alles — 0 Titel. +# **Ein abgelehnter Schluessel ist schlimmer als gar keiner.** + + +def test_unter_windows_geht_der_key_in_die_registry(): + gelandet = [] + auf.key_ablegen("T-abc", plattform_name="win32", + schreiber=lambda k: gelandet.append(k) or r"HKCU\Software\MakeMKV") + assert gelandet == ["T-abc"] + + +def test_unter_linux_bleibt_es_die_datei(): + """Der Container-Weg darf sich durch die Reparatur NICHT aendern.""" + gelandet = [] + wohin = auf.key_ablegen("T-abc", plattform_name="linux", + datei_schreiber=lambda k: gelandet.append(k) or "/root/.MakeMKV/settings.conf") + assert gelandet == ["T-abc"] + assert wohin.endswith("settings.conf") + + +def test_ohne_key_wird_nichts_geschrieben(): + """Sonst stuende ein leerer app_Key da — und der ist ein ungueltiger.""" + assert auf.key_ablegen("", plattform_name="win32", + schreiber=lambda k: 1 / 0) == "" + assert auf.key_ablegen(None, plattform_name="win32", + schreiber=lambda k: 1 / 0) == "" + + +AUSGABE_ABGELEHNT = ( + 'MSG:1005,0,1,"MakeMKV v1.18.4 win(x64-release) gestartet"\n' + 'MSG:5020,0,0,"Der hinterlegte Aktivierungsschlüssel ist ungültig."\n' + 'MSG:5021,0,0,"Diese Programmversion ist zu alt."\n' +).encode("utf-8") + +AUSGABE_GUT = ( + 'MSG:1005,0,1,"MakeMKV v1.18.4 win(x64-release) gestartet"\n' + 'DRV:0,2,999,12,"BD-RE BU40N","","G:"\n' +).encode("utf-8") + + +def test_ein_abgelehnter_schluessel_wird_erkannt(): + urteil = auf.key_pruefen("makemkvcon", laufen=lambda p: AUSGABE_ABGELEHNT) + assert urteil["ok"] is False + assert urteil["code"] == 5020 + assert "nicht angenommen" in urteil["grund"] + + +def test_ein_angenommener_schluessel_auch(): + assert auf.key_pruefen("makemkvcon", laufen=lambda p: AUSGABE_GUT)["ok"] is True + + +def test_ohne_makemkv_wird_nichts_behauptet(): + """„Nicht befragbar" ist keine Aussage ueber den Schluessel.""" + urteil = auf.key_pruefen("") + assert urteil["ok"] is False + assert "nicht gefunden" in urteil["grund"] + + +def test_ein_abgelehnter_schluessel_wird_WIEDER_ENTFERNT(): + """Der Kern des Befundes: Ohne Key las MakeMKV die Disc noch, mit dem + abgelehnten verweigerte es alles. Blind schreiben verschlechtert.""" + geschrieben, entfernt = [], [] + ergebnis = auf.key_setzen_und_pruefen( + "T-zualt", "makemkvcon", plattform_name="win32", + schreiber=lambda k: geschrieben.append(k) or r"HKCU\Software\MakeMKV", + pruefer=lambda p: {"ok": False, "code": 5021, + "grund": auf.KRITISCHE_CODES[5021]}, + entferner=lambda: entfernt.append(True) or True) + assert geschrieben == ["T-zualt"] + assert entfernt == [True], "der abgelehnte Schluessel muss wieder weg" + assert ergebnis["ok"] is False + assert "zu alt" in ergebnis["grund"] + + +def test_ein_angenommener_schluessel_bleibt_liegen(): + entfernt = [] + ergebnis = auf.key_setzen_und_pruefen( + "T-gut", "makemkvcon", plattform_name="win32", + schreiber=lambda k: r"HKCU\Software\MakeMKV", + pruefer=lambda p: {"ok": True, "code": None, "grund": ""}, + entferner=lambda: entfernt.append(True) or True) + assert ergebnis["ok"] is True + assert entfernt == [], "ein gueltiger Schluessel darf nicht angefasst werden" + + +def test_die_codes_stehen_so_da_wie_gemessen(): + """Waechter gegen den Fehler, den ich am 29.08.2026 selbst gemacht habe: + 5021 aus dem Kopf mit „Volume-Key unbekannt" zu beschriften.""" + assert "zu alt" in auf.KRITISCHE_CODES[5021] + assert "Volume" not in auf.KRITISCHE_CODES[5021] + assert auf.KEY_ABGELEHNT == (5020, 5021) diff --git a/src/rippy/setup_fenster.py b/src/rippy/setup_fenster.py index 0a8608f..194f259 100644 --- a/src/rippy/setup_fenster.py +++ b/src/rippy/setup_fenster.py @@ -371,7 +371,22 @@ def _beta_key_holen(melden=None) -> str: "Einstellungen.") return "" _einstellungen_schreiben({"makemkvAppKey": key}) - melden(" MakeMKV-Beta-Key geholt (…%s)." % key[-6:]) + + # Und jetzt nachsehen, ob MakeMKV ihn ueberhaupt annimmt. Am 29.08.2026 + # tat es das nicht: Version 1.18.4 ist aelter als der aktuelle Schluessel + # verlangt (MSG 5020 + 5021). Ein abgelehnter Schluessel ist SCHLIMMER als + # gar keiner — ohne las MakeMKV die Disc noch, mit dem abgelehnten + # verweigerte es alles. `key_setzen_und_pruefen` nimmt ihn deshalb wieder + # weg. Begruendung in `rippy.rip.makemkv_aufruf.key_pruefen`. + from rippy.rip import makemkv_aufruf + from rippy.tools import katalog + + urteil = makemkv_aufruf.key_setzen_und_pruefen(key, katalog.finden("makemkv")) + if urteil["ok"]: + melden(" MakeMKV-Beta-Key geholt und angenommen (…%s)." % key[-6:]) + else: + melden(" MakeMKV-Beta-Key geholt, aber NICHT angenommen: %s" + % urteil["grund"]) return key