35cfcbcb07
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>
245 lines
10 KiB
Python
245 lines
10 KiB
Python
"""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 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 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
|
|
|
|
WARUM DER DATEINAME "_worker" HINTEN DRANHAENGT (25.07.2026): makemkv_daten.py
|
|
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 zurückbenennen.
|
|
"""
|
|
|
|
import hashlib
|
|
import importlib.util
|
|
import os
|
|
|
|
# WICHTIG (Prüfbefund 25.07.2026): Ein schlichtes "from makemkv_daten import ..."
|
|
# lädt bei "pytest -q" vom Repo-Wurzelverzeichnis NICHT diese Datei, sondern die
|
|
# API-Kopie — docker/api wird zuerst gesammelt, und jeder weitere Import trifft
|
|
# nur noch den sys.modules-Cache. Die Tests hier hätten den Worker-Zwilling also
|
|
# nie angefasst und eine Abweichung wäre grün durchgelaufen. Deshalb wird er
|
|
# ausdrücklich über seinen Pfad geladen.
|
|
_HIER = os.path.dirname(os.path.abspath(__file__))
|
|
_WORKER_MODUL = os.path.join(_HIER, "makemkv_daten.py")
|
|
_API_MODUL = os.path.abspath(os.path.join(_HIER, "..", "api", "makemkv_daten.py"))
|
|
|
|
_spec = importlib.util.spec_from_file_location("makemkv_daten_worker_kopie", _WORKER_MODUL)
|
|
_modul = importlib.util.module_from_spec(_spec)
|
|
_spec.loader.exec_module(_modul)
|
|
|
|
ist_aacs_dump = _modul.ist_aacs_dump
|
|
keydb_pruefen = _modul.keydb_pruefen
|
|
settings_conf_zusammenfuehren = _modul.settings_conf_zusammenfuehren
|
|
zaehle_disc_eintraege = _modul.zaehle_disc_eintraege
|
|
private_data_pruefen = _modul.private_data_pruefen
|
|
zaehle_schluessel = _modul.zaehle_schluessel
|
|
|
|
|
|
def _tar_mit(namen):
|
|
"""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 für die Schluessel, dazu Index-Dateien.
|
|
"""
|
|
import io
|
|
import tarfile
|
|
|
|
puffer = io.BytesIO()
|
|
with tarfile.open(fileobj=puffer, mode="w") as archiv:
|
|
for name in namen:
|
|
eintrag = tarfile.TarInfo(name)
|
|
eintrag.size = 3
|
|
archiv.addfile(eintrag, io.BytesIO(b"abc"))
|
|
return puffer.getvalue()
|
|
|
|
|
|
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" für einen komplett leeren Vorrat.
|
|
voll = _tar_mit([
|
|
"hkd_00000059.bin",
|
|
"hkd_0000005a.bin",
|
|
"sdf_000000a6.bin",
|
|
"--index-A2E950B3C3FC57DA9CB856DCAFBA5275F40423DB.bin",
|
|
])
|
|
assert zaehle_schluessel(voll) == 2
|
|
|
|
|
|
def test_zaehle_schluessel_leerer_speicher_ist_null():
|
|
# Genau dieser Zustand lag am 25.07.2026 auf der VM vor: ein Archiv mit
|
|
# ausschliesslich der Index-Datei. Jede unbekannte UHD-Disc scheitert dann.
|
|
leer = _tar_mit(["--index-4EF73C3560D489497ACE763962A7F07A4A1545C4.bin"])
|
|
assert zaehle_schluessel(leer) == 0
|
|
|
|
|
|
def test_zaehle_schluessel_bei_muell_kein_absturz():
|
|
# Darf niemals werfen — die Zahl landet im Worker-Herzschlag.
|
|
assert zaehle_schluessel(b"") == 0
|
|
assert zaehle_schluessel(b"das ist kein tar") == 0
|
|
|
|
|
|
def test_private_data_pruefen_nimmt_echten_speicher_an():
|
|
assert private_data_pruefen(_tar_mit(["hkd_00000059.bin"])) == ""
|
|
|
|
|
|
def test_private_data_pruefen_lehnt_leere_und_falsche_dateien_ab():
|
|
assert "leer" in private_data_pruefen(b"")
|
|
assert "tar-Archiv" in private_data_pruefen(b"<html>Fehlerseite</html>")
|
|
|
|
|
|
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 Prüfung ändert
|
|
# sich nichts und niemand versteht, warum.
|
|
leer = _tar_mit(["--index-4EF73C3560D489497ACE763962A7F07A4A1545C4.bin"])
|
|
assert "kein einziger Schluessel" in private_data_pruefen(leer)
|
|
|
|
|
|
def test_zwillinge_sind_byteweise_identisch():
|
|
"""docker/api/makemkv_daten.py MUSS dieselbe Datei sein wie diese hier.
|
|
|
|
Das Modul existiert bewusst doppelt — es gibt in diesem Projekt kein
|
|
gemeinsames Paket für API und Worker (gleiche Lage wie bei db.py). Genau
|
|
deshalb braucht es einen Wächter: laufen die beiden auseinander, zeigt das
|
|
UI etwas anderes an, als der rippende Worker tatsächlich sieht, und es
|
|
fällt niemandem auf. Dieser Test ist die einzige Stelle, die das
|
|
mechanisch prüft.
|
|
"""
|
|
with open(_WORKER_MODUL, "rb") as datei:
|
|
worker = hashlib.sha256(datei.read()).hexdigest()
|
|
with open(_API_MODUL, "rb") as datei:
|
|
api = hashlib.sha256(datei.read()).hexdigest()
|
|
assert worker == api, (
|
|
"docker/worker/makemkv_daten.py und docker/api/makemkv_daten.py sind "
|
|
"auseinandergelaufen - Änderungen immer in BEIDE Dateien übernehmen."
|
|
)
|
|
|
|
# Eine kleine, aber echte KEYDB.cfg im libaacs-Format: Kommentarkopf, eine
|
|
# Disc-Zeile MIT 0x-Praefix, eine OHNE, dazu ein Fortsetzungsfeld und eine
|
|
# Leerzeile. Erwartete Zahl der Eintraege: 2.
|
|
BEISPIEL_KEYDB = """; KEYDB.cfg
|
|
; Kommentarzeilen beginnen mit einem Semikolon
|
|
|
|
0x8F4E2C1A9B7D3E5F0A6C8B2D4E1F3A5C7B9D0E2F = AKIRA
|
|
| V | 0123456789ABCDEF0123456789ABCDEF
|
|
|
|
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 dürfen nicht mitzaehlen —
|
|
sonst meldet das UI bei einer reinen Kommentardatei stolz "42 Eintraege".
|
|
"""
|
|
assert zaehle_disc_eintraege(BEISPIEL_KEYDB) == 2
|
|
|
|
|
|
def test_zaehle_disc_eintraege_ignoriert_kommentare_und_leerzeilen():
|
|
# Eine Datei ganz ohne Disc-Zeile hat null Eintraege, nicht drei.
|
|
nur_beiwerk = "; nur ein Kommentar\n\n| V | 0123456789ABCDEF0123456789ABCDEF\n"
|
|
assert zaehle_disc_eintraege(nur_beiwerk) == 0
|
|
|
|
|
|
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 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():
|
|
# Häufigster Fehlgriff: das Textfeld war leer, es wird trotzdem gespeichert.
|
|
assert keydb_pruefen("") != ""
|
|
assert keydb_pruefen(" \n\n ") != ""
|
|
|
|
|
|
def test_keydb_pruefen_erkennt_html_fehlerseite():
|
|
"""Der zweithaeufigste Fehlgriff: der Download lieferte eine HTML-Seite.
|
|
|
|
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"
|
|
meldung = keydb_pruefen(html)
|
|
assert meldung != ""
|
|
assert "HTML" in meldung
|
|
|
|
|
|
def test_keydb_pruefen_meldet_text_ohne_disc_zeile():
|
|
# Irgendein Text (hier: eine README) ist keine KEYDB.cfg.
|
|
meldung = keydb_pruefen("Diese Datei enthält keine Schluessel, nur Prosa.\n")
|
|
assert meldung != ""
|
|
|
|
|
|
def test_keydb_pruefen_akzeptiert_gueltigen_inhalt():
|
|
# Leerer Rueckgabewert heißt laut Vertrag: alles in Ordnung.
|
|
assert keydb_pruefen(BEISPIEL_KEYDB) == ""
|
|
|
|
|
|
def test_ist_aacs_dump_akzeptiert_echten_namen():
|
|
"""Name aus der Praxis: so legt MakeMKV den Dump laut Meldung 3332 ab
|
|
(am 25.07.2026 im Worker so beobachtet)."""
|
|
assert ist_aacs_dump("MKB20_v76_UHD_AKIRA_C02B.tgz") is True
|
|
|
|
|
|
def test_ist_aacs_dump_lehnt_pfad_tricks_und_fremde_dateien_ab():
|
|
"""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
|
|
assert ist_aacs_dump("irgendwas.txt") is False
|
|
assert ist_aacs_dump("..\\windows\\ausbruch.tgz") is False
|
|
|
|
|
|
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 ü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")
|
|
assert 'app_Key = "T-neuerSchluessel"' in neu
|
|
assert 'app_Key = "T-alterSchluessel"' not in neu
|
|
assert 'app_UpdateEnable = "1"' in neu
|
|
assert 'app_DefaultSelectionString = "+sel:all"' in neu
|
|
# Genau EINE app_Key-Zeile, sonst gewinnt am Ende die falsche.
|
|
assert neu.count("app_Key") == 1
|
|
|
|
|
|
def test_settings_conf_leerer_key_entfernt_die_zeile():
|
|
# Ein bewusst geleerter Key darf nicht heimlich weiterwirken.
|
|
alt = 'app_Key = "T-alterSchluessel"\napp_UpdateEnable = "1"\n'
|
|
neu = settings_conf_zusammenfuehren(alt, "")
|
|
assert "app_Key" not in neu
|
|
assert 'app_UpdateEnable = "1"' in neu
|
|
|
|
|
|
def test_settings_conf_aus_dem_nichts_ergibt_saubere_datei():
|
|
"""Erststart: die Datei gibt es noch gar nicht. Der abschliessende
|
|
Zeilenumbruch ist Absicht — MakeMKV liest die Datei zeilenweise."""
|
|
assert settings_conf_zusammenfuehren("", "T-neuerSchluessel") == 'app_Key = "T-neuerSchluessel"\n'
|
|
|
|
|
|
def test_settings_conf_ohne_key_und_ohne_inhalt_bleibt_leer():
|
|
# Kein Inhalt, kein Key: keine Datei mit einer einsamen Leerzeile erzeugen.
|
|
assert settings_conf_zusammenfuehren("", "") == ""
|