fix(windows): Rippen ging nicht — drei Container-Annahmen im Rip-Pfad
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
73ed104bbc
commit
954f293871
@@ -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
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user