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:
Hitonabi
2026-08-29 13:52:06 +02:00
co-authored by Claude Opus 5
parent 73ed104bbc
commit 954f293871
9 changed files with 693 additions and 39 deletions
+20 -4
View File
@@ -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")
+5 -4
View File
@@ -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:
+13 -4
View File
@@ -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.'
+18 -11
View File
@@ -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}")
+32 -11
View File
@@ -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:
+315
View File
@@ -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
+59 -4
View File
@@ -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"
+215
View File
@@ -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)
+16 -1
View File
@@ -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