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
+20
-4
@@ -2445,14 +2445,30 @@ async def makemkv_key_holen():
|
|||||||
detail="Im Forum-Beitrag stand kein Key im erwarteten Format. "
|
detail="Im Forum-Beitrag stand kein Key im erwarteten Format. "
|
||||||
"Vermutlich hat sich die Seite geändert.")
|
"Vermutlich hat sich die Seite geändert.")
|
||||||
geaendert = await asyncio.to_thread(key_modul._apply_key, key)
|
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(
|
await asyncio.to_thread(
|
||||||
db.add_log, "success", "makemkv-key",
|
db.add_log, "success" if urteil["ok"] else "warning", "makemkv-key",
|
||||||
"Beta-Key aus dem Forum geholt (…%s)%s"
|
"Beta-Key aus dem Forum geholt (…%s)%s%s"
|
||||||
% (key[-6:], "" if geaendert else " — war schon der aktuelle"))
|
% (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,
|
# Der Key selbst wird NICHT zurueckgegeben: Er steht in den Einstellungen,
|
||||||
# und ein zweiter Weg zu demselben Wert ist ein zweiter Weg, ihn zu
|
# und ein zweiter Weg zu demselben Wert ist ein zweiter Weg, ihn zu
|
||||||
# verlieren.
|
# 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")
|
@app.post("/system/keydb")
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import subprocess
|
|||||||
from typing import Dict, List, Optional
|
from typing import Dict, List, Optional
|
||||||
|
|
||||||
from rippy import drives as _laufwerks_schicht
|
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 rippy.tools import katalog as werkzeug_katalog
|
||||||
from clients.tmdb import TMDBClient
|
from clients.tmdb import TMDBClient
|
||||||
from clients.jikan import JikanClient
|
from clients.jikan import JikanClient
|
||||||
@@ -365,12 +367,11 @@ class PreScan:
|
|||||||
makemkv = werkzeug_katalog.finden("makemkv")
|
makemkv = werkzeug_katalog.finden("makemkv")
|
||||||
if makemkv:
|
if makemkv:
|
||||||
result = subprocess.run(
|
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,
|
capture_output=True,
|
||||||
text=True,
|
timeout=120 # binaer lesen — Begruendung in text_von
|
||||||
timeout=120
|
|
||||||
)
|
)
|
||||||
for line in result.stdout.split('\n'):
|
for line in text_von(result.stdout).split('\n'):
|
||||||
if line.startswith('TINFO:'):
|
if line.startswith('TINFO:'):
|
||||||
parts = line.split(',')
|
parts = line.split(',')
|
||||||
if len(parts) >= 5:
|
if len(parts) >= 5:
|
||||||
|
|||||||
@@ -270,10 +270,19 @@ export default function SettingsPage() {
|
|||||||
const r = await api.post('/system/makemkv-key/holen')
|
const r = await api.post('/system/makemkv-key/holen')
|
||||||
const antwort = await api.get('/settings')
|
const antwort = await api.get('/settings')
|
||||||
setSettings({ ...defaultSettings, ...antwort.data })
|
setSettings({ ...defaultSettings, ...antwort.data })
|
||||||
setKeyMeldung(r.data?.geaendert
|
if (r.data?.angenommen === false) {
|
||||||
? `Neuer Key geholt (endet auf …${r.data.endet_auf}).`
|
// Geholt ist nicht angenommen. Am 29.08.2026 lehnte MakeMKV 1.18.4
|
||||||
: `Schon aktuell (endet auf …${r.data?.endet_auf || '?'}).`)
|
// den aktuellen Key ab („Programmversion zu alt") — ohne diesen
|
||||||
toast('success', 'MakeMKV-Beta-Key aus dem Forum geholt.')
|
// 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) {
|
} catch (e: any) {
|
||||||
const grund = e?.response?.data?.detail
|
const grund = e?.response?.data?.detail
|
||||||
|| 'Das Forum ist nicht erreichbar.'
|
|| 'Das Forum ist nicht erreichbar.'
|
||||||
|
|||||||
+18
-11
@@ -47,7 +47,7 @@ import time
|
|||||||
import requests
|
import requests
|
||||||
|
|
||||||
from rippy import store as db
|
from rippy import store as db
|
||||||
from rippy.rip import makemkv_daten
|
from rippy.rip import makemkv_aufruf, makemkv_daten
|
||||||
import medien
|
import medien
|
||||||
from rippy.core import notify
|
from rippy.core import notify
|
||||||
|
|
||||||
@@ -243,17 +243,24 @@ def _makemkv_key_anwenden(einstellungen: dict) -> None:
|
|||||||
key = (einstellungen.get("makemkvAppKey") or "").strip()
|
key = (einstellungen.get("makemkvAppKey") or "").strip()
|
||||||
if not key:
|
if not key:
|
||||||
return
|
return
|
||||||
pfad = os.path.join(makemkv_daten.DATEN_DIR, "settings.conf")
|
|
||||||
try:
|
try:
|
||||||
os.makedirs(makemkv_daten.DATEN_DIR, exist_ok=True)
|
# WOHIN, entscheidet der Rechner — unter Windows die Registry, sonst
|
||||||
try:
|
# die settings.conf. Begruendung und Messung in
|
||||||
with open(pfad, encoding="utf-8", errors="replace") as f:
|
# `rippy.rip.makemkv_aufruf.key_ablegen`.
|
||||||
alt = f.read()
|
#
|
||||||
except OSError:
|
# UND nachsehen, ob MakeMKV ihn annimmt: Ein abgelehnter Schluessel
|
||||||
alt = ""
|
# ist schlimmer als gar keiner — ohne las MakeMKV die Disc noch, mit
|
||||||
with open(pfad, "w", encoding="utf-8", newline="\n") as f:
|
# einem abgelehnten verweigerte es alles (gemessen 29.08.2026). Die
|
||||||
f.write(makemkv_daten.settings_conf_zusammenfuehren(alt, key))
|
# paar Sekunden Rueckfrage sind nichts gegen einen Rip, der Stunden
|
||||||
except OSError as e:
|
# 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}")
|
db.add_log("warning", "worker", f"MakeMKV-Key konnte nicht gesetzt werden: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+32
-11
@@ -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.drives.linux import auswerfen_versuchen as wirf_disc_aus # noqa: F401
|
||||||
from rippy.platform.winlauf import OHNE_FENSTER
|
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
|
from rippy.tools import katalog as werkzeuge
|
||||||
|
|
||||||
_NACKTER_NAME = {"makemkv": "makemkvcon", "handbrake": "HandBrakeCLI"}
|
_NACKTER_NAME = {"makemkv": "makemkvcon", "handbrake": "HandBrakeCLI"}
|
||||||
@@ -102,7 +104,7 @@ def build_makemkv_cmd(device_path: str, output_dir: str, titel: str = "all") ->
|
|||||||
"--noscan",
|
"--noscan",
|
||||||
"--progress=-same",
|
"--progress=-same",
|
||||||
"mkv",
|
"mkv",
|
||||||
f"dev:{device_path}",
|
makemkv_quelle(device_path),
|
||||||
str(titel),
|
str(titel),
|
||||||
output_dir,
|
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.
|
wäre eine Minute Wartezeit für nichts.
|
||||||
"""
|
"""
|
||||||
ergebnis = subprocess.run(
|
ergebnis = subprocess.run(
|
||||||
[werkzeug("makemkv"), "-r", "--noscan", "info", f"dev:{device_path}"],
|
[werkzeug("makemkv"), "-r", "--noscan", "info", makemkv_quelle(device_path)],
|
||||||
capture_output=True, text=True, timeout=timeout,
|
capture_output=True, timeout=timeout, # binaer: siehe text_von
|
||||||
creationflags=OHNE_FENSTER,
|
creationflags=OHNE_FENSTER,
|
||||||
)
|
)
|
||||||
ausgabe = ergebnis.stdout or ""
|
ausgabe = text_von(ergebnis.stdout)
|
||||||
return parse_titel_info(ausgabe), parse_stream_info(ausgabe)
|
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)."""
|
"""Fragt die Titel-Laufzeiten der Disc ab (makemkvcon info, Robot-Mode)."""
|
||||||
try:
|
try:
|
||||||
ergebnis = subprocess.run(
|
ergebnis = subprocess.run(
|
||||||
[werkzeug("makemkv"), "-r", "--noscan", "info", f"dev:{device_path}"],
|
[werkzeug("makemkv"), "-r", "--noscan", "info", makemkv_quelle(device_path)],
|
||||||
capture_output=True, text=True, timeout=timeout,
|
capture_output=True, timeout=timeout, # binaer: siehe text_von
|
||||||
creationflags=OHNE_FENSTER,
|
creationflags=OHNE_FENSTER,
|
||||||
)
|
)
|
||||||
except (OSError, subprocess.TimeoutExpired):
|
except (OSError, subprocess.TimeoutExpired):
|
||||||
return {}
|
return {}
|
||||||
return parse_tinfo_dauern(ergebnis.stdout or "")
|
return parse_tinfo_dauern(text_von(ergebnis.stdout))
|
||||||
|
|
||||||
|
|
||||||
def parse_scan_dauer(ausgabe: str) -> int:
|
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),
|
build_makemkv_cmd(device_path, output_dir, titel),
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.STDOUT,
|
stderr=subprocess.STDOUT,
|
||||||
text=True,
|
# BINAER lesen und selbst dekodieren (siehe `text_von`):
|
||||||
bufsize=1,
|
# `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,
|
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
|
# abgelaufen) steht Zeilen davor und ging im Fehlertext verloren
|
||||||
# (Befund 24.07., Summer-Wars-UHD).
|
# (Befund 24.07., Summer-Wars-UHD).
|
||||||
kritische_meldungen = []
|
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 = (
|
KRITISCH = (
|
||||||
"volume key is unknown",
|
"volume key is unknown",
|
||||||
"evaluation period has expired",
|
"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",
|
"Too old version",
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
for line in process.stdout:
|
for rohzeile in process.stdout:
|
||||||
|
line = text_von(rohzeile)
|
||||||
progress = get_progress_from_prgv(line)
|
progress = get_progress_from_prgv(line)
|
||||||
if progress >= 0:
|
if progress >= 0:
|
||||||
if progress_cb:
|
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:
|
if meldung is None:
|
||||||
continue
|
continue
|
||||||
code, letzte_meldung = meldung
|
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)
|
kritische_meldungen.append(letzte_meldung)
|
||||||
if log_cb:
|
if log_cb:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -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 io
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
import tarfile
|
import tarfile
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
# Container-Pfad aus der Umgebung: /root/.MakeMKV im Worker (nur dort sucht
|
def daten_verzeichnis(umgebung=None, plattform_name=None, existiert=None) -> str:
|
||||||
# makemkvcon), /app/makemkv-data in der API. Beide zeigen laut
|
"""Wo makemkvcon SEINE Einstellungen sucht — je Rechner ein anderer Ort.
|
||||||
# docker-compose.yml auf DASSELBE Host-Verzeichnis.
|
|
||||||
DATEN_DIR = os.getenv("MAKEMKV_DATA_DIR", "/root/.MakeMKV")
|
## 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.
|
# GROSS geschrieben — unter Linux case-sensitiv, "keydb.cfg" wird ignoriert.
|
||||||
KEYDB_NAME = "KEYDB.cfg"
|
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.")
|
"Einstellungen.")
|
||||||
return ""
|
return ""
|
||||||
_einstellungen_schreiben({"makemkvAppKey": key})
|
_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
|
return key
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user