style: echte Umlaute im ganzen Projekt + Installer im Rippy-Look
Ampel / ampel (push) Successful in 27s

Commander-Rueckmeldung: Umlaute fehlen. Ausloeser war sichtbar der Installer -
"Diese Maschine uebernimmt die Video-Kompression fuer Rippy" stand woertlich im
Screenshot.

## Die .ps1-Falle war loesbar, nicht unumgehbar

Bisher galt: ausgelieferte .ps1 MUESSEN ASCII sein, weil PowerShell 5.1 sie
ohne BOM als ANSI liest. Das ist nur die halbe Wahrheit - gemessen mit echtem
powershell.exe 5.1:

    ohne BOM:  $s = "Größe: äöü"   + Unerwartetes Token -> Skript kaputt
    mit  BOM:  Groesse: aeoeue         + laeuft, Length 16 korrekt

Alle drei Skripte sind jetzt UTF-8 MIT BOM und tragen echte Umlaute; unter 5.1
gegengeprueft (BOM vorhanden, Parser fehlerfrei, Text korrekt gelesen). Der
Kopfkommentar sagt das jetzt richtig statt "ASCII-only".

## Umstellung: Text ja, Bezeichner nein

Umlaute gehoeren in Kommentare und Anzeigetexte, nicht in Funktionsnamen oder
Datenschluessel. Deshalb je Sprache das passende Werkzeug:

- Python: ueber den TOKENIZER - angefasst wurden ausschliesslich COMMENT- und
  STRING-Tokens. 156 Stellen. Code ist damit garantiert unberuehrt.
- TypeScript: nur // und /* */ Kommentare sowie JSX-Text (kann per Definition
  kein Bezeichner sein). 24 Stellen. `const waehlen`, `let laeuft`, `plaetze`,
  `GeraetInfo` sind nachweislich unversehrt.
- install.sh: Anzeigetext, aber die Shell-Funktionen (gruen/rot/gelb/titel) und
  der Schalter --nur-pruefen bleiben ASCII - das sind Schnittstellen.
- Markdown: 0 Aenderungen, die Doku hatte schon Umlaute.

ZWEI FEHLER MEINES KONVERTERS, beide von Werkzeugen gefangen:

1. In f-Strings steht in {...} CODE, kein Text. Aus f"{groesse}" wurde
   f"{größe}", waehrend die Variable groesse hiess - Ruff meldete F821
   "Undefined name". Der Konverter lagert Einsetzungen jetzt aus.
2. Ein Dict-Schluessel wurde umbenannt: die Wortliste enthaelt das PRAEFIX
   "uebersprung", der Tabu-Schutz prueft aber ganze Woerter. bericht[...] ist
   wieder ASCII - Umlaute in Datenschluesseln brechen JSON-Runden und DB-Felder.

## Installer im Rippy-Look

Statt hellgrau jetzt dieselben Toene wie das Web-UI (aus lib/design.ts
uebernommen): slate-900 Flaeche, dunkle Eingabefelder, Amber-Hauptknopf wie
"Los geht's" im Wizard. Oben ein Kopfbereich mit dem Farbverlauf
amber -> indigo -> purple und dem Disc-Symbol - in WinForms per Paint-Ereignis
gezeichnet, weil es dort keine Verlaeufe von der Stange gibt.

Geprueft, nicht gehofft: Das Fenster wurde headless in ein PNG gerendert
(DrawToBitmap) und angesehen - Verlauf, Symbol, Umlaute und Farben sitzen.
RippyWorkerSetup.exe neu gebaut (57344 -> 60416 Bytes); Umlaute und das
requireAdministrator-Manifest sind in der .exe verifiziert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-25 22:37:23 +02:00
parent 65edf8369e
commit 35cfcbcb07
24 changed files with 702 additions and 640 deletions
+1 -1
View File
@@ -203,7 +203,7 @@ def zaehle_online_worker(sekunden: int = 120) -> int:
Die Zombie-Erkennung vergleicht das mit der Zahl der Celery-Antworten:
melden sich weniger Worker als bekannt sind, ist die Auskunft
unvollstaendig — dann wird NICHTS als Leiche gewertet.
unvollständig — dann wird NICHTS als Leiche gewertet.
"""
from datetime import timedelta
+44 -44
View File
@@ -1,8 +1,8 @@
"""MakeMKV-Datenverzeichnis: Schluesselspeicher, KEYDB.cfg, AACS-Dumps.
"""MakeMKV-Datenverzeichnis: Schlüsselspeicher, KEYDB.cfg, AACS-Dumps.
WARUM ES DIESE DATEI GIBT (Befund 25.07.2026, auf BEIDEN Maschinen gemessen):
4K-UHD-Discs scheiterten mit "The volume key is unknown for this disc". Die
Ursache ist weder die Disc noch das Laufwerk — LibreDrive v06.3 laeuft und
Ursache ist weder die Disc noch das Laufwerk — LibreDrive v06.3 läuft und
MakeMKV liest die Disc — sondern:
makemkvcon unter LINUX ruft die Disc-Schluessel nie ab.
@@ -10,35 +10,35 @@ MakeMKV liest die Disc — sondern:
Gemessen, nicht vermutet:
* Linux: in KEINEM Lauf auch nur eine Verbindung nach draussen. Geprueft mit
leerem UND mit gefuelltem Schluesselspeicher, mit und ohne --noscan, mit
dev:/dev/sr0 und mit disc:0, und mit erzwungener frischer Pruefung
(update.conf geloescht, Meldung 5074 belegt den Web-Kontakt). Immer:
leerem UND mit gefülltem Schlüsselspeicher, mit und ohne --noscan, mit
dev:/dev/sr0 und mit disc:0, und mit erzwungener frischer Prüfung
(update.conf gelöscht, Meldung 5074 belegt den Web-Kontakt). Immer:
keine Verbindung, kein Schluessel.
* Windows, dieselbe Disc, dasselbe Laufwerk: Meldung 3338 "Downloading
latest HK to ...", Verbindung nach 185.84.108.20:443
(web33.majordomo.ru), _private_data.tar waechst — und die Disc geht auf
(web33.majordomo.ru), _private_data.tar wächst — und die Disc geht auf
(TCOUNT:5, "Operation successfully completed").
* Der Code dafuer steckt auch im Linux-Binary: die Meldungsvorlage
"Downloading latest %1 to %2 ..." steht in makemkvcon. Sie loest nur nie
* Der Code dafür steckt auch im Linux-Binary: die Meldungsvorlage
"Downloading latest %1 to %2 ..." steht in makemkvcon. Sie löst nur nie
aus. Gleiches Symptom im Forum, seit Jahren offen und unbeantwortet.
FRUEHERE FEHLDIAGNOSE, bewusst festgehalten: An dieser Stelle stand zuerst,
MakeMKVs Schluessel-Kanal sei abgeschaltet. Das war FALSCH. Die Herleitung
stuetzte sich auf zwei Hostnamen aus alten Forumsbeitraegen
stützte sich auf zwei Hostnamen aus alten Forumsbeitraegen
(hkdata.fairuse.org, hkdata.crabdance.com), die tatsaechlich nicht mehr
aufloesen — MakeMKV benutzt sie aber laengst nicht mehr. Der Dienst lebt, der
Worker erreicht ihn sogar; er wird unter Linux nur nie gefragt.
WAS DARAUS FOLGT: Der Schluesselspeicher (_private_data.tar) muss von einer
WAS DARAUS FOLGT: Der Schlüsselspeicher (_private_data.tar) muss von einer
MakeMKV-Installation kommen, die ihn wirklich abruft — praktisch von Windows.
Rippy nimmt ihn unter Einstellungen -> System entgegen. Die KEYDB.cfg bleibt
der Notnagel fuer Pressungen, die auch MakeMKV selbst nicht kennt.
der Notnagel für Pressungen, die auch MakeMKV selbst nicht kennt.
Rippy liefert KEINE Schluessel mit, laedt keine herunter und verteilt keine.
Rippy liefert KEINE Schluessel mit, lädt keine herunter und verteilt keine.
Es verwaltet nur, was der Nutzer selbst mitbringt.
QUELLEN (AGENTS Regel D — externe Schnittstellen nie aus dem Kopf):
* Linux laedt keine Hashed Keys — dasselbe Symptom, mehrfach berichtet:
* Linux lädt keine Hashed Keys — dasselbe Symptom, mehrfach berichtet:
https://forum.makemkv.com/forum/viewtopic.php?t=25782
https://forum.makemkv.com/forum/viewtopic.php?t=34022
* Schluessel liegen als hkd_*.bin in _private_data.tar:
@@ -55,7 +55,7 @@ QUELLEN (AGENTS Regel D — externe Schnittstellen nie aus dem Kopf):
ZWILLINGSDATEI: liegt identisch unter docker/api/ und docker/worker/ — beide
Images brauchen sie, ein gemeinsames Paket gibt es in diesem Projekt nicht
(gleiche Lage wie bei db.py). Aenderungen IMMER in BEIDEN Dateien nachziehen.
(gleiche Lage wie bei db.py). Änderungen IMMER in BEIDEN Dateien nachziehen.
"""
import io
@@ -72,14 +72,14 @@ DATEN_DIR = os.getenv("MAKEMKV_DATA_DIR", "/root/.MakeMKV")
# GROSS geschrieben — unter Linux case-sensitiv, "keydb.cfg" wird ignoriert.
KEYDB_NAME = "KEYDB.cfg"
# Obergrenze fuer den Upload. Eine vollstaendige oeffentliche KEYDB.cfg liegt
# Obergrenze für den Upload. Eine vollständige oeffentliche KEYDB.cfg liegt
# im einstelligen MB-Bereich; 64 MB sind reichlich Luft und verhindern, dass
# eine versehentlich hochgeladene Riesendatei den Speicher vollschreibt.
MAX_KEYDB_BYTES = 64 * 1024 * 1024
# Eine Disc-Zeile beginnt mit der 40 Zeichen langen Hex-Kennung (optional mit
# 0x davor), danach folgt das Gleichheitszeichen. Alles andere (Kommentare ab
# ";", Leerzeilen, Fortsetzungsfelder) zaehlt nicht als Eintrag.
# ";", Leerzeilen, Fortsetzungsfelder) zählt nicht als Eintrag.
_DISC_ZEILE = re.compile(r"^\s*(?:0x)?[0-9a-fA-F]{40}\s*=")
@@ -94,9 +94,9 @@ def keydb_pfad(daten_dir: str = None) -> str:
def zaehle_disc_eintraege(inhalt: str) -> int:
"""Zeilen mit Disc-Kennung zaehlen (pure Funktion, testbar).
"""Zeilen mit Disc-Kennung zählen (pure Funktion, testbar).
Bewusst eine Heuristik und keine vollstaendige Auswertung: MakeMKV liest
Bewusst eine Heuristik und keine vollständige Auswertung: MakeMKV liest
die Datei mit seinem eigenen Parser, und wie viele Eintraege es daraus
macht, ist von aussen nicht sichtbar. Die Zahl dient nur dazu, im UI
"da liegt wirklich etwas drin" von "leere oder falsche Datei" zu
@@ -106,17 +106,17 @@ def zaehle_disc_eintraege(inhalt: str) -> int:
def keydb_pruefen(inhalt: str) -> str:
"""Prueft hochgeladenen Inhalt; gibt deutschen Fehlertext oder "" zurueck.
"""Prüft hochgeladenen Inhalt; gibt deutschen Fehlertext oder "" zurück.
Verhindert den haeufigsten Bedienfehler: statt der KEYDB.cfg landet die
Verhindert den häufigsten Bedienfehler: statt der KEYDB.cfg landet die
HTML-Fehlerseite eines Downloads oder eine leere Datei im Verzeichnis —
MakeMKV wuerde dann still weiter "volume key is unknown" melden.
MakeMKV würde dann still weiter "volume key is unknown" melden.
"""
if not inhalt.strip():
return "Die Datei ist leer."
if len(inhalt.encode("utf-8")) > MAX_KEYDB_BYTES:
return (
"Die Datei ist groesser als "
"Die Datei ist größer als "
f"{MAX_KEYDB_BYTES // (1024 * 1024)} MB — das ist keine KEYDB.cfg."
)
if inhalt.lstrip()[:1] == "<":
@@ -150,7 +150,7 @@ def keydb_status(daten_dir: str = None) -> dict:
with open(pfad, encoding="utf-8", errors="replace") as datei:
eintraege = zaehle_disc_eintraege(datei.read())
except OSError:
pass # Datei da, aber unlesbar: Groesse/Datum stimmen trotzdem
pass # Datei da, aber unlesbar: Größe/Datum stimmen trotzdem
return {
"vorhanden": True,
"pfad": pfad,
@@ -163,7 +163,7 @@ def keydb_status(daten_dir: str = None) -> dict:
def keydb_schreiben(inhalt: str, daten_dir: str = None) -> dict:
"""Schreibt die KEYDB.cfg atomar und meldet den neuen Zustand.
Erst in eine Nebendatei, dann os.replace: waehrend ein Rip laeuft, darf
Erst in eine Nebendatei, dann os.replace: während ein Rip läuft, darf
makemkvcon niemals eine halb geschriebene Datei zu sehen bekommen.
"""
pfad = keydb_pfad(daten_dir)
@@ -244,32 +244,32 @@ def dumps_auflisten(daten_dir: str = None) -> list:
return liste
# --- Schluesselspeicher (_private_data.tar) -------------------------------
# --- Schlüsselspeicher (_private_data.tar) -------------------------------
#
# Das ist MakeMKVs eigener Speicher fuer die "Hashed Keys": ein tar-Archiv mit
# hkd_*.bin-Eintraegen. Unter Windows fuellt MakeMKV es selbst (Meldung 3338),
# Das ist MakeMKVs eigener Speicher für die "Hashed Keys": ein tar-Archiv mit
# hkd_*.bin-Eintraegen. Unter Windows füllt MakeMKV es selbst (Meldung 3338),
# unter Linux nie — siehe Modul-Kopf. Rippy nimmt die Datei deshalb entgegen
# und legt sie ins Datenverzeichnis; MakeMKV liest sie beim naechsten Start.
# und legt sie ins Datenverzeichnis; MakeMKV liest sie beim nächsten Start.
PRIVATE_DATA_NAME = "_private_data.tar"
# Der Speicher lag am 25.07.2026 bei rund 6 MB und waechst mit jeder neuen
# Der Speicher lag am 25.07.2026 bei rund 6 MB und wächst mit jeder neuen
# Pressung. 64 MB sind reichlich Luft — und derselbe Wert wie
# client_max_body_size in docker/ui/nginx.conf: waere die Grenze hier hoeher,
# wuerde nginx den Upload abweisen, bevor die API ihn ueberhaupt sieht.
# client_max_body_size in docker/ui/nginx.conf: wäre die Grenze hier höher,
# würde nginx den Upload abweisen, bevor die API ihn überhaupt sieht.
MAX_PRIVATE_DATA_BYTES = 64 * 1024 * 1024
def private_data_pfad(daten_dir: str = None) -> str:
"""Voller Pfad zum Schluesselspeicher im Datenverzeichnis."""
"""Voller Pfad zum Schlüsselspeicher im Datenverzeichnis."""
return os.path.join(daten_dir or DATEN_DIR, PRIVATE_DATA_NAME)
def zaehle_schluessel(rohdaten: bytes) -> int:
"""Anzahl der hkd_*.bin-Eintraege im Archiv (pure Funktion, testbar).
Das ist die ehrliche Kennzahl fuer "wie viele Disc-Schluessel kennt diese
Installation". Ein frischer, leerer Speicher enthaelt nur eine
Das ist die ehrliche Kennzahl für "wie viele Disc-Schluessel kennt diese
Installation". Ein frischer, leerer Speicher enthält nur eine
Index-Datei und kommt hier auf 0 — genau der Zustand, in dem jede
unbekannte UHD-Disc scheitert.
"""
@@ -281,20 +281,20 @@ def zaehle_schluessel(rohdaten: bytes) -> int:
def private_data_pruefen(rohdaten: bytes) -> str:
"""Prueft hochgeladene Rohdaten; deutscher Fehlertext oder "".
"""Prüft hochgeladene Rohdaten; deutscher Fehlertext oder "".
Faengt die beiden Bedienfehler ab, die sonst still danebengehen: eine
voellig andere Datei hochladen, oder den Speicher einer Installation, die
selbst noch keine Schluessel geholt hat (dann aendert sich nichts, und
selbst noch keine Schluessel geholt hat (dann ändert sich nichts, und
niemand versteht warum).
"""
if not rohdaten:
return "Die Datei ist leer."
if len(rohdaten) > MAX_PRIVATE_DATA_BYTES:
return (
"Die Datei ist groesser als "
"Die Datei ist größer als "
f"{MAX_PRIVATE_DATA_BYTES // (1024 * 1024)} MB — das ist kein "
"MakeMKV-Schluesselspeicher."
"MakeMKV-Schlüsselspeicher."
)
try:
with tarfile.open(fileobj=io.BytesIO(rohdaten)) as archiv:
@@ -308,13 +308,13 @@ def private_data_pruefen(rohdaten: bytes) -> str:
return (
"In dieser Datei steckt kein einziger Schluessel (kein hkd_*.bin). "
"Sie stammt vermutlich von einer MakeMKV-Installation, die selbst "
"noch keine geholt hat — oeffne dort erst einmal eine Disc."
"noch keine geholt hat — öffne dort erst einmal eine Disc."
)
return ""
def schluesselspeicher_status(daten_dir: str = None) -> dict:
"""Zustand des Schluesselspeichers. Fehlt er, ist das kein Fehler."""
"""Zustand des Schlüsselspeichers. Fehlt er, ist das kein Fehler."""
pfad = private_data_pfad(daten_dir)
try:
angaben = os.stat(pfad)
@@ -338,9 +338,9 @@ def schluesselspeicher_status(daten_dir: str = None) -> dict:
def private_data_schreiben(rohdaten: bytes, daten_dir: str = None) -> dict:
"""Legt den Schluesselspeicher atomar ab und meldet den neuen Zustand.
"""Legt den Schlüsselspeicher atomar ab und meldet den neuen Zustand.
Atomar aus demselben Grund wie bei der KEYDB.cfg: waehrend ein Rip laeuft,
Atomar aus demselben Grund wie bei der KEYDB.cfg: während ein Rip läuft,
darf makemkvcon nie ein halb geschriebenes Archiv sehen.
"""
pfad = private_data_pfad(daten_dir)
@@ -363,9 +363,9 @@ def settings_conf_zusammenfuehren(inhalt: str, key: str) -> str:
"""app_Key setzen, ohne den Rest der settings.conf zu verlieren (pure).
Bis zum 25.07.2026 haben entrypoint.sh UND tasks.py die Datei komplett
ueberschrieben. Mit dem jetzt persistenten Datenverzeichnis waere damit
überschrieben. Mit dem jetzt persistenten Datenverzeichnis wäre damit
bei jedem Containerstart und vor jedem Rip alles andere weg — z. B.
app_UpdateEnable. Leerer Key laesst die vorhandene Zeile ebenfalls fallen,
app_UpdateEnable. Leerer Key lässt die vorhandene Zeile ebenfalls fallen,
damit ein bewusst geleerter Key nicht heimlich weiterwirkt.
"""
zeilen = [z for z in inhalt.splitlines() if not z.lstrip().startswith("app_Key")]
+4 -4
View File
@@ -223,9 +223,9 @@ def parse_msg(zeile: str):
fertig zusammengesetzte Klartext.
Bis zum 25.07.2026 stand hier line.split(",", 4)[3]: das schnitt JEDE
Meldung ab, die selbst ein Komma enthaelt — und MakeMKV schreibt solche
Meldung ab, die selbst ein Komma enthält — und MakeMKV schreibt solche
laufend ("Title #1 has length of 12 seconds, which is less than ...").
Deshalb eine Regex, die die Anfuehrungszeichen respektiert.
Deshalb eine Regex, die die Anführungszeichen respektiert.
"""
treffer = _MSG_RE.match(zeile.strip())
if not treffer:
@@ -409,7 +409,7 @@ def _handbrake_schleife(process, output_path: str, abbruch_cb=None, progress_cb=
try:
for line in process.stdout:
# Zuerst der Abbruch — unabhängig davon, ob die Zeile überhaupt
# einen Fortschritt enthält. Sonst haengt „Abbrechen" daran, dass
# einen Fortschritt enthält. Sonst hängt „Abbrechen" daran, dass
# sich die Prozentzahl bewegt (Befund 25.07.2026).
if abbruch_cb:
abbruch_cb()
@@ -475,7 +475,7 @@ def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str
log_cb(code, text) bekommt JEDE MakeMKV-Meldung. Bewusst ein eigener
Kanal statt progress_cb: der Fortschritts-Callback in tasks.py verwirft
Aufrufe, bei denen sich die Prozentzahl nicht geaendert hat — Meldungen
waeren dort also grossteils verschwunden. Ohne diesen Kanal war am
wären dort also großteils verschwunden. Ohne diesen Kanal war am
25.07.2026 nicht von aussen erkennbar, dass MakeMKV bei der UHD-Disc
nicht einmal versucht, einen Schluessel zu holen (siehe makemkv_daten).
"""
+1 -1
View File
@@ -104,7 +104,7 @@ def unter_wurzel(pfad: str, wurzel: str) -> bool:
"""Liegt `pfad` wirklich unterhalb von `wurzel` (oder IST es die Wurzel)?
Pure Funktion, testbar. Ein nacktes `startswith()` genügt nicht:
„/app/media-boese/x" beginnt mit „/app/media", liegt aber ausserhalb
„/app/media-boese/x" beginnt mit „/app/media", liegt aber außerhalb
(Befund 25.07.2026 bei der Durchsicht). Immer „/" als Trenner — das sind
Container-Pfade, auch wenn ein nativer Windows-Worker das Modul lädt.
+21 -21
View File
@@ -1,13 +1,13 @@
"""Tests fuer makemkv_daten.py: die reinen Helfer rund um das MakeMKV-Datenverzeichnis.
"""Tests für makemkv_daten.py: die reinen Helfer rund um das MakeMKV-Datenverzeichnis.
WARUM ES DIESE TESTS GIBT (Befund 25.07.2026, live im Worker nachgemessen):
Eine 4K-UHD-Disc (Akira UHD, MKB v76) scheiterte mit "The volume key is unknown
for this disc", obwohl Laufwerk und MakeMKV in Ordnung waren. Der einzige heute
noch funktionierende Weg ist eine selbst mitgebrachte KEYDB.cfg im
Datenverzeichnis. Damit haengt einiges an diesen kleinen Funktionen: erkennen wir
die Datei falsch, meldet das UI "alles gut", waehrend MakeMKV weiter scheitert.
Datenverzeichnis. Damit hängt einiges an diesen kleinen Funktionen: erkennen wir
die Datei falsch, meldet das UI "alles gut", während MakeMKV weiter scheitert.
Getestet wird nur, was ohne Postgres, Redis und ohne Laufwerk laeuft — also die
Getestet wird nur, was ohne Postgres, Redis und ohne Laufwerk läuft — also die
puren Funktionen mit echten Beispieldaten. Zeilenformat der KEYDB.cfg laut
libaacs (AGENTS Regel D, externe Schnittstellen nie aus dem Kopf):
https://github.com/ShiftMediaProject/libaacs/blob/master/KEYDB.cfg
@@ -17,7 +17,7 @@ ist eine Zwillingsdatei, es gibt sie unter docker/api/ UND docker/worker/, und
beide Seiten haben Tests. Da im Projekt keine __init__.py liegen, importiert
pytest Testdateien unter ihrem blossen Dateinamen — zwei Dateien namens
test_makemkv_daten.py brechen deshalb die Sammelphase ab ("import file
mismatch") und faerben die ganze Ampel rot. Nicht zurueckbenennen.
mismatch") und faerben die ganze Ampel rot. Nicht zurückbenennen.
"""
import hashlib
@@ -47,10 +47,10 @@ zaehle_schluessel = _modul.zaehle_schluessel
def _tar_mit(namen):
"""Baut ein tar-Archiv im Speicher — so sieht MakeMKVs Schluesselspeicher aus.
"""Baut ein tar-Archiv im Speicher — so sieht MakeMKVs Schlüsselspeicher aus.
Echte Eintragsnamen aus dem Speicher der Windows-Installation vom
25.07.2026: hkd_<8 Hex>.bin fuer die Schluessel, dazu Index-Dateien.
25.07.2026: hkd_<8 Hex>.bin für die Schluessel, dazu Index-Dateien.
"""
import io
import tarfile
@@ -67,7 +67,7 @@ def _tar_mit(namen):
def test_zaehle_schluessel_zaehlt_nur_hkd_eintraege():
# Nur hkd_*.bin sind Disc-Schluessel. Index- und sdf-Dateien gehoeren zum
# Speicher dazu, sind aber keine Schluessel — sonst meldete das UI
# "1 Schluessel vorhanden" fuer einen komplett leeren Vorrat.
# "1 Schluessel vorhanden" für einen komplett leeren Vorrat.
voll = _tar_mit([
"hkd_00000059.bin",
"hkd_0000005a.bin",
@@ -101,7 +101,7 @@ def test_private_data_pruefen_lehnt_leere_und_falsche_dateien_ab():
def test_private_data_pruefen_lehnt_speicher_ohne_schluessel_ab():
# Der teuerste Bedienfehler: den Speicher einer Installation hochladen,
# die selbst noch nie Schluessel geholt hat. Ohne diese Pruefung aendert
# die selbst noch nie Schluessel geholt hat. Ohne diese Prüfung ändert
# sich nichts und niemand versteht, warum.
leer = _tar_mit(["--index-4EF73C3560D489497ACE763962A7F07A4A1545C4.bin"])
assert "kein einziger Schluessel" in private_data_pruefen(leer)
@@ -123,7 +123,7 @@ def test_zwillinge_sind_byteweise_identisch():
api = hashlib.sha256(datei.read()).hexdigest()
assert worker == api, (
"docker/worker/makemkv_daten.py und docker/api/makemkv_daten.py sind "
"auseinandergelaufen - Aenderungen immer in BEIDE Dateien uebernehmen."
"auseinandergelaufen - Änderungen immer in BEIDE Dateien übernehmen."
)
# Eine kleine, aber echte KEYDB.cfg im libaacs-Format: Kommentarkopf, eine
@@ -142,7 +142,7 @@ A1B2C3D4E5F60718293A4B5C6D7E8F90A1B2C3D4 = BLADE RUNNER 2049
def test_zaehle_disc_eintraege_zaehlt_nur_echte_disc_zeilen():
"""Nur Zeilen mit 40 Hex-Zeichen und Gleichheitszeichen sind Eintraege.
Kommentare, Leerzeilen und Fortsetzungsfelder duerfen nicht mitzaehlen —
Kommentare, Leerzeilen und Fortsetzungsfelder dürfen nicht mitzaehlen —
sonst meldet das UI bei einer reinen Kommentardatei stolz "42 Eintraege".
"""
assert zaehle_disc_eintraege(BEISPIEL_KEYDB) == 2
@@ -158,14 +158,14 @@ def test_zaehle_disc_eintraege_ignoriert_zu_kurze_kennung():
"""39 Hex-Zeichen sind keine Disc-Kennung.
Genau so sieht eine beim Kopieren verstuemmelte Datei aus — die darf nicht
als gueltig durchgehen, sonst sucht der Commander den Fehler beim Laufwerk.
als gültig durchgehen, sonst sucht der Commander den Fehler beim Laufwerk.
"""
zu_kurz = "A1B2C3D4E5F60718293A4B5C6D7E8F90A1B2C3D = KAPUTT\n"
assert zaehle_disc_eintraege(zu_kurz) == 0
def test_keydb_pruefen_meldet_leere_datei():
# Haeufigster Fehlgriff: das Textfeld war leer, es wird trotzdem gespeichert.
# Häufigster Fehlgriff: das Textfeld war leer, es wird trotzdem gespeichert.
assert keydb_pruefen("") != ""
assert keydb_pruefen(" \n\n ") != ""
@@ -173,7 +173,7 @@ def test_keydb_pruefen_meldet_leere_datei():
def test_keydb_pruefen_erkennt_html_fehlerseite():
"""Der zweithaeufigste Fehlgriff: der Download lieferte eine HTML-Seite.
MakeMKV wuerde die Datei still ignorieren und weiter "volume key is unknown"
MakeMKV würde die Datei still ignorieren und weiter "volume key is unknown"
melden — deshalb muss der Fehler schon beim Hochladen sichtbar werden.
"""
html = "<!DOCTYPE html>\n<html><body><h1>404 Not Found</h1></body></html>\n"
@@ -184,12 +184,12 @@ def test_keydb_pruefen_erkennt_html_fehlerseite():
def test_keydb_pruefen_meldet_text_ohne_disc_zeile():
# Irgendein Text (hier: eine README) ist keine KEYDB.cfg.
meldung = keydb_pruefen("Diese Datei enthaelt keine Schluessel, nur Prosa.\n")
meldung = keydb_pruefen("Diese Datei enthält keine Schluessel, nur Prosa.\n")
assert meldung != ""
def test_keydb_pruefen_akzeptiert_gueltigen_inhalt():
# Leerer Rueckgabewert heisst laut Vertrag: alles in Ordnung.
# Leerer Rueckgabewert heißt laut Vertrag: alles in Ordnung.
assert keydb_pruefen(BEISPIEL_KEYDB) == ""
@@ -200,9 +200,9 @@ def test_ist_aacs_dump_akzeptiert_echten_namen():
def test_ist_aacs_dump_lehnt_pfad_tricks_und_fremde_dateien_ab():
"""Der Download-Endpunkt haengt den Namen an das Datenverzeichnis an —
ohne diese Pruefung koennte man sich damit aus dem Verzeichnis heraus
lesen. Versteckte Dateien und Nicht-Dumps sind ebenfalls nichts fuer die
"""Der Download-Endpunkt hängt den Namen an das Datenverzeichnis an —
ohne diese Prüfung könnte man sich damit aus dem Verzeichnis heraus
lesen. Versteckte Dateien und Nicht-Dumps sind ebenfalls nichts für die
Liste."""
assert ist_aacs_dump("../ausbruch.tgz") is False
assert ist_aacs_dump(".versteckt.tgz") is False
@@ -212,8 +212,8 @@ def test_ist_aacs_dump_lehnt_pfad_tricks_und_fremde_dateien_ab():
def test_settings_conf_ersetzt_key_und_behaelt_den_rest():
"""DIE Regression, um die es geht: bis zum 25.07.2026 haben entrypoint.sh
und tasks.py die settings.conf komplett ueberschrieben. Mit dem jetzt
persistenten Datenverzeichnis waere damit bei jedem Containerstart und vor
und tasks.py die settings.conf komplett überschrieben. Mit dem jetzt
persistenten Datenverzeichnis wäre damit bei jedem Containerstart und vor
jedem Rip alles andere weg — allen voran app_UpdateEnable."""
alt = 'app_Key = "T-alterSchluessel"\napp_UpdateEnable = "1"\napp_DefaultSelectionString = "+sel:all"\n'
neu = settings_conf_zusammenfuehren(alt, "T-neuerSchluessel")
+9 -9
View File
@@ -114,15 +114,15 @@ def test_pfad_lokal_uebersetzt_fuer_windows_worker():
assert pfad_lokal("/app/temp/raw/abc", "") == "/app/temp/raw/abc"
# Die beiden frueheren Tests zu _original_aufheben sind nach
# test_original_aufheben.py gewandert und dort erweitert. Sie pruefen jetzt das
# Die beiden früheren Tests zu _original_aufheben sind nach
# test_original_aufheben.py gewandert und dort erweitert. Sie prüfen jetzt das
# richtige Kriterium: Vorher taeuschten sie per gefaelschtem os.stat
# „verschiedene Dateisysteme" vor — genau die Annahme, an der der Schutz in der
# Praxis scheiterte (st_dev war identisch, os.rename trotzdem EXDEV).
def test_arbeitsverzeichnis_wahl_des_rips_schlaegt_die_einstellung():
"""Pro Rip waehlbar (Commander 25.07.2026), Einstellung bleibt Standard.
"""Pro Rip wählbar (Commander 25.07.2026), Einstellung bleibt Standard.
Reihenfolge: Wahl dieses Rips -> Setting -> Container-Default. Der
Setting-Wert ist genau der, der bei Vollautomatik-Rips greift, weil dort
@@ -134,16 +134,16 @@ def test_arbeitsverzeichnis_wahl_des_rips_schlaegt_die_einstellung():
assert tasks._arbeitsverzeichnis(einst, "/app/media/rippy") == "/app/media/rippy"
assert tasks._arbeitsverzeichnis(einst) == "/app/media/movies"
assert tasks._arbeitsverzeichnis({}) == tasks.RAW_DIR
# Ausbruchsversuche und Pfade ausserhalb /app/media fallen durch
# Ausbruchsversuche und Pfade außerhalb /app/media fallen durch
assert tasks._arbeitsverzeichnis({}, "/etc") == tasks.RAW_DIR
assert tasks._arbeitsverzeichnis({}, "/app/media/../etc") == tasks.RAW_DIR
# Leere Wahl faellt sauber auf die Einstellung zurueck
# Leere Wahl fällt sauber auf die Einstellung zurück
assert tasks._arbeitsverzeichnis(einst, " ") == "/app/media/movies"
def test_unter_wurzel_faellt_nicht_auf_praefix_namen_herein():
"""Befund 25.07.2026: Elf Stellen prueften mit nacktem startswith().
„/app/media-boese/x" beginnt mit „/app/media", liegt aber ausserhalb."""
"""Befund 25.07.2026: Elf Stellen prüften mit nacktem startswith().
„/app/media-boese/x" beginnt mit „/app/media", liegt aber außerhalb."""
import tasks
assert tasks.unter_wurzel("/app/media", "/app/media") is True
@@ -155,7 +155,7 @@ def test_unter_wurzel_faellt_nicht_auf_praefix_namen_herein():
assert tasks.unter_wurzel("/etc/passwd", "/app/media") is False
assert tasks.unter_wurzel("", "/app/media") is False
assert tasks.unter_wurzel("/app/media", "") is False
# Wurzel mit Schrägstrich am Ende darf nichts aendern
# Wurzel mit Schrägstrich am Ende darf nichts ändern
assert tasks.unter_wurzel("/app/media/movies", "/app/media/") is True
@@ -163,7 +163,7 @@ def test_zielbasis_lehnt_praefix_ausbruch_ab():
import tasks
assert tasks._zielbasis("/app/media/movies", "bluray") == "/app/media/movies"
# Ausbruch per Praefix-Namen faellt auf den Standard zurueck
# Ausbruch per Praefix-Namen fällt auf den Standard zurück
assert tasks._zielbasis("/app/media-boese", "bluray") != "/app/media-boese"
assert tasks._zielbasis("/etc", "bluray") != "/etc"
+6 -6
View File
@@ -1,11 +1,11 @@
"""Tests fuer _original_aufheben — der Pfad, der am 25.07.2026 die Platte fuellte.
"""Tests für _original_aufheben — der Pfad, der am 25.07.2026 die Platte füllte.
Der Kern: Es wird NICHT vorhergesagt, ob umgehaengt werden kann, sondern
os.rename versucht. Die alte Fassung verglich st_dev und lag damit falsch —
auf der Rippy-VM sind st_dev von /app/temp und /app/media identisch (2050),
os.rename scheitert zwischen ihnen trotzdem mit EXDEV, weil der Kernel den
Mount vergleicht und nicht das Geraet. Die Platzpruefung wurde deshalb
uebersprungen und shutil.move kopierte 75 GB bei 37 GB frei.
Mount vergleicht und nicht das Gerät. Die Platzpruefung wurde deshalb
übersprungen und shutil.move kopierte 75 GB bei 37 GB frei.
"""
import errno
@@ -46,7 +46,7 @@ def _lege_rohdaten_an(tmp_path, groesse=2048):
def test_umhaengen_wenn_derselbe_mount(tmp_path, fake_db):
"""Der gute Fall: rename klappt, nichts wird kopiert, kein Platz noetig."""
"""Der gute Fall: rename klappt, nichts wird kopiert, kein Platz nötig."""
raw, final = _lege_rohdaten_an(tmp_path)
tasks._original_aufheben("job-1", raw, final)
@@ -58,9 +58,9 @@ def test_umhaengen_wenn_derselbe_mount(tmp_path, fake_db):
def test_bei_exdev_und_zu_wenig_platz_wird_nur_gewarnt(tmp_path, fake_db, monkeypatch):
"""Der Fall, der die Platte fuellte: rename geht nicht, Platz reicht nicht.
"""Der Fall, der die Platte füllte: rename geht nicht, Platz reicht nicht.
Vorher lief hier eine Vollkopie an, weil die st_dev-Pruefung „gleiches
Vorher lief hier eine Vollkopie an, weil die st_dev-Prüfung „gleiches
Dateisystem" meldete und die Platzpruefung deshalb ausblieb.
"""
raw, final = _lege_rohdaten_an(tmp_path)
+15 -15
View File
@@ -113,7 +113,7 @@ def test_parse_msg_trennt_code_und_klartext():
def test_parse_msg_liest_die_uhd_fehlermeldung():
"""3303 ist der Befund, um den es beim ganzen KEYDB-Thema geht: das
Laufwerk laeuft im LibreDrive-Modus, MakeMKV kennt nur den Schluessel
Laufwerk läuft im LibreDrive-Modus, MakeMKV kennt nur den Schluessel
DIESER Pressung nicht. Ohne diese Zeile im Log raet der Commander."""
assert parse_msg(
'MSG:3303,16777216,0,"The volume key is unknown for this disc - video can\'t be decrypted","The volume key is unknown for this disc - video can\'t be decrypted"'
@@ -125,9 +125,9 @@ def test_parse_msg_liest_die_uhd_fehlermeldung():
def test_parse_msg_schneidet_meldungen_mit_komma_nicht_ab():
"""DER Grund fuer die Regex (Stand 25.07.2026): vorher stand hier
"""DER Grund für die Regex (Stand 25.07.2026): vorher stand hier
line.split(",", 4)[3]. Das schnitt jede Meldung ab, die selbst ein Komma
enthaelt — und MakeMKV schreibt solche laufend. Im Log stand dann nur noch
enthält — und MakeMKV schreibt solche laufend. Im Log stand dann nur noch
ein Satzfragment, das mehr verwirrt als hilft."""
zeile = (
'MSG:3025,0,3,"Title #1 has length of 12 seconds, which is less than '
@@ -142,7 +142,7 @@ def test_parse_msg_schneidet_meldungen_mit_komma_nicht_ab():
def test_parse_msg_ignoriert_fremde_zeilen():
# Alles ausser MSG muss None liefern, sonst landet Fortschritts-Rauschen
# Alles außer MSG muss None liefern, sonst landet Fortschritts-Rauschen
# (PRGV kommt mehrmals pro Sekunde) als Log-Eintrag in der Datenbank.
assert parse_msg("PRGV:100,32768,65536") is None
assert parse_msg('DRV:0,2,999,12,"BD-RE ASUS BW-16D1HT","AKIRA","/dev/sr0"') is None
@@ -188,8 +188,8 @@ def test_abcde_config_enthaelt_zielverzeichnis():
def test_preset_fuer_nimmt_das_preset_des_disc_typs():
# Befund 25.07.2026: vorher galt EIN Preset fuer alles — eine 4K-UHD wurde
# damit auf 1080p heruntergerechnet, und beim ersten echten UHD-Rip waere
# Befund 25.07.2026: vorher galt EIN Preset für alles — eine 4K-UHD wurde
# damit auf 1080p heruntergerechnet, und beim ersten echten UHD-Rip wäre
# die 4K-Aufloesung still verlorengegangen.
from ripping import preset_fuer
@@ -206,13 +206,13 @@ def test_preset_fuer_nimmt_das_preset_des_disc_typs():
def test_preset_fuer_faellt_auf_das_allgemeine_preset_zurueck():
# Bestandsinstallationen kennen die drei neuen Felder nicht. Solange der
# Nutzer sie nicht speichert, MUSS sich sein Verhalten nicht aendern.
# Nutzer sie nicht speichert, MUSS sich sein Verhalten nicht ändern.
from ripping import preset_fuer
alt = {"transcodePreset": "HQ 1080p30 Surround"}
assert preset_fuer("uhd", alt) == "HQ 1080p30 Surround"
assert preset_fuer("dvd", alt) == "HQ 1080p30 Surround"
# Leere Zeichenkette zaehlt als "nicht gesetzt" (leeres Select-Feld im UI)
# Leere Zeichenkette zählt als "nicht gesetzt" (leeres Select-Feld im UI)
assert preset_fuer("uhd", {"transcodePresetUhd": " ", "transcodePreset": "X"}) == "X"
@@ -269,7 +269,7 @@ def test_handbrake_prueft_abbruch_bei_jeder_zeile_nicht_nur_bei_fortschritt():
assert ergebnis["status"] == "cancelled"
assert prozess.getoetet is True
# Zwei Scan-Zeilen genuegten — es musste NICHT auf eine Encode-Zeile gewartet
# Zwei Scan-Zeilen genügten — es musste NICHT auf eine Encode-Zeile gewartet
# werden. Genau das war der Fehler.
assert len(gesehen) == 2
@@ -279,16 +279,16 @@ def test_komprimieren_fuer_globaler_schalter_schlaegt_alles():
assert komprimieren_fuer("uhd", {"transcodeEnabled": False}) is False
assert komprimieren_fuer("dvd", {"transcodeEnabled": False}) is False
# Standard ist AN - Bestandsinstallationen aendern ihr Verhalten nicht
# Standard ist AN - Bestandsinstallationen ändern ihr Verhalten nicht
assert komprimieren_fuer("bluray", {}) is True
assert komprimieren_fuer("bluray", None) is True
def test_komprimieren_fuer_einzelner_disc_typ_abwaehlbar():
"""Der Grund dafuer (Befund 25.07.2026): 4K-HEVC brauchte auf der Rippy-VM
"""Der Grund dafür (Befund 25.07.2026): 4K-HEVC brauchte auf der Rippy-VM
gemessene 28-55 Stunden, weil die CPU kein AVX2 hat. Vorher gab es nur
einen globalen Schalter - wer 4K verlustfrei behalten und DVDs trotzdem
schrumpfen wollte, hatte keine Moeglichkeit."""
schrumpfen wollte, hatte keine Möglichkeit."""
from ripping import PRESET_KEINE, komprimieren_fuer
nur_uhd_aus = {"transcodePresetUhd": PRESET_KEINE,
@@ -299,20 +299,20 @@ def test_komprimieren_fuer_einzelner_disc_typ_abwaehlbar():
def test_preset_fuer_gibt_den_reservewert_nie_als_preset_zurueck():
"""Sonst bekaeme HandBrake `--preset keine` und wuerde scheitern. Wer ueber
"""Sonst bekaeme HandBrake `--preset keine` und würde scheitern. Wer über
"Neu komprimieren" ausdruecklich doch komprimieren will, soll ein
brauchbares Preset bekommen statt eines Fehlers."""
from ripping import DEFAULT_HB_PRESET, PRESET_KEINE, preset_fuer
assert preset_fuer("uhd", {"transcodePresetUhd": PRESET_KEINE,
"transcodePreset": "HQ 1080p30 Surround"}) == "HQ 1080p30 Surround"
# Auch ohne allgemeines Preset kommt ein gueltiger Name heraus
# Auch ohne allgemeines Preset kommt ein gültiger Name heraus
assert preset_fuer("uhd", {"transcodePresetUhd": PRESET_KEINE}) == DEFAULT_HB_PRESET
def test_reservewert_kollidiert_mit_keinem_echten_preset():
"""Gegengeprueft gegen alle 90 Presets aus `HandBrakeCLI --preset-list`
im Worker-Image (25.07.2026) - keines heisst so."""
im Worker-Image (25.07.2026) - keines heißt so."""
from ripping import PRESET_KEINE
assert PRESET_KEINE == "keine"