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
+4 -4
View File
@@ -58,9 +58,9 @@ async def startup_event():
asyncio.create_task(asyncio.to_thread(remount))
asyncio.create_task(disc_watcher())
# MakeMKV-Beta-Key automatisch aktuell halten (wechselt ~monatlich, laeuft zum
# MakeMKV-Beta-Key automatisch aktuell halten (wechselt ~monatlich, läuft zum
# Monatsende ab) — sonst blockt Blu-ray-Ripping irgendwann still. Taeglicher
# Forum-Abgleich; wirkt ohne Rebuild ab dem naechsten Rip.
# Forum-Abgleich; wirkt ohne Rebuild ab dem nächsten Rip.
asyncio.create_task(makemkv_key.refresh_loop())
# Worker-Erreichbarkeit im Hintergrund pingen (siehe _ping_knoten) — sonst
# kostet JEDER Aufruf von /capabilities eine ganze Sekunde.
@@ -467,7 +467,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 hier 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). Deshalb Gleichheit ODER Wurzel
samt Trennzeichen. Erwartet werden normalisierte Container-Pfade mit „/".
"""
@@ -1383,7 +1383,7 @@ async def set_keystore(request: Request):
rohdaten = await request.body()
def pruefe_und_schreibe():
# Prüfung liest ein mehrere MB grosses tar — gehört deshalb mit in
# Prüfung liest ein mehrere MB großes tar — gehört deshalb mit in
# den Thread und nicht in die Ereignisschleife.
fehler = makemkv_daten.private_data_pruefen(rohdaten)
if fehler:
+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")]
+9 -9
View File
@@ -1,22 +1,22 @@
"""Automatische Erneuerung des MakeMKV-Beta-Keys.
Der kostenlose MakeMKV-Beta-Key (Forum-Thread "MakeMKV is free while in beta",
t=1053) wechselt etwa monatlich und laeuft zum Monatsende ab. Ohne gueltigen Key
faellt Blu-ray/UHD-Ripping in den 30-Tage-Testmodus; DVD-Ripping bleibt frei.
t=1053) wechselt etwa monatlich und läuft zum Monatsende ab. Ohne gültigen Key
fällt Blu-ray/UHD-Ripping in den 30-Tage-Testmodus; DVD-Ripping bleibt frei.
Bisher musste der Key von Hand nachgetragen werden (UI: Einstellungen -> System).
Dieses Modul holt den aktuellen Key vom Forum und schreibt ihn in die Settings
(makemkvAppKey). Der Worker liest den Key VOR jedem Rip aus den Settings
(docker/worker/tasks.py) und schreibt settings.conf -> die Erneuerung wirkt ohne
Rebuild/Neustart, ab dem naechsten Rip.
Rebuild/Neustart, ab dem nächsten Rip.
Wichtig: Das ist die kostenlose, oeffentliche Beta-LIZENZ der Software selbst —
NICHT das Entschluesseln oder Verteilen von Disc-Schluesseln. Diese Grenze gilt
unveraendert; Rippy liefert keine Disc-Schluessel mit und verteilt keine.
Richtigstellung 25.07.2026: Hier stand frueher, MakeMKV ziehe den AACS-Schluessel
via LibreDrive ohnehin selbst aus dem Laufwerk. Das stimmt fuer Blu-ray, aber
NICHT fuer 4K-UHD — dort braucht MakeMKV den Volume-Key der jeweiligen Pressung,
Richtigstellung 25.07.2026: Hier stand früher, MakeMKV ziehe den AACS-Schluessel
via LibreDrive ohnehin selbst aus dem Laufwerk. Das stimmt für Blu-ray, aber
NICHT für 4K-UHD — dort braucht MakeMKV den Volume-Key der jeweiligen Pressung,
und den bekommt es weder aus dem Laufwerk noch (heute) aus dem Netz. Belege und
Messungen stehen im Modul-Kopf von makemkv_daten.py.
@@ -41,14 +41,14 @@ def extract_key(html: str) -> str | None:
def fetch_current_key(timeout: int = 15) -> str | None:
"""Holt den aktuellen Beta-Key vom MakeMKV-Forum. None bei Fehler/kein Treffer."""
import requests # lazy: haelt extract_key + den Test dependency-frei
import requests # lazy: hält extract_key + den Test dependency-frei
antwort = requests.get(FORUM_URL, headers=_HEADERS, timeout=timeout)
antwort.raise_for_status()
return extract_key(antwort.text)
def _apply_key(key: str) -> bool:
"""Merge den Key in die Settings. save_settings ueberschreibt das GANZE JSON,
"""Merge den Key in die Settings. save_settings überschreibt das GANZE JSON,
darum erst lesen, dann setzen. Rueckgabe True, wenn sich der Key geaendert hat."""
import db
einstellungen = db.get_settings()
@@ -76,7 +76,7 @@ async def refresh_once() -> bool:
async def refresh_loop(intervall_stunden: int = 24, start_verzoegerung_s: int = 60) -> None:
"""Periodische Erneuerung (Default: taeglich, damit ein Monatswechsel nie verpasst
wird). Fehler werden geloggt, aber nie geworfen -> die API laeuft weiter."""
wird). Fehler werden geloggt, aber nie geworfen -> die API läuft weiter."""
import db
await asyncio.sleep(start_verzoegerung_s)
while True:
+3 -3
View File
@@ -76,11 +76,11 @@ def schreibtest(pfad: str) -> bool:
def _stale_mounts_loesen(ziel: str) -> int:
"""Loest ALLE (evtl. gestapelten/toten) Mounts an `ziel` per lazy umount.
Warum: mounten() faellt bei einem TOTEN Mount (os.path.ismount wirft OSError)
Warum: mounten() fällt bei einem TOTEN Mount (os.path.ismount wirft OSError)
auf den echten `mount` durch — der stapelt dann auf die Leiche. Vorfall 24.07.:
ueber viele Neustarts 12 Schichten, die tote oberste blockierte jeden Zugriff
über viele Neustarts 12 Schichten, die tote oberste blockierte jeden Zugriff
(ls-Timeout, obwohl SMB-445 offen). Erst alle Schichten loesen macht das
Re-Mounten idempotent. `umount -l` (lazy) haengt nicht an einem toten CIFS.
Re-Mounten idempotent. `umount -l` (lazy) hängt nicht an einem toten CIFS.
Rueckgabe: Zahl der geloesten Schichten.
"""
geloest = 0
+9 -9
View File
@@ -24,8 +24,8 @@ def test_main_importierbar_und_routen_verdrahtet():
# Schlüsseldatei (Befund 25.07.2026 — MakeMKV holt UHD-Schlüssel nicht
# mehr online nach). Ohne diese Routen ist die Seite im UI tot.
"/system/keydb", "/system/aacs-dumps", "/system/aacs-dumps/{dateiname}",
# Der Hauptweg fuer 4K-UHD (25.07.2026): makemkvcon holt Disc-Schluessel
# unter Linux nie selbst, sie kommen von Hand ueber diesen Endpunkt.
# Der Hauptweg für 4K-UHD (25.07.2026): makemkvcon holt Disc-Schluessel
# unter Linux nie selbst, sie kommen von Hand über diesen Endpunkt.
"/system/keystore",
):
assert pfad in routen, f"Route {pfad} fehlt"
@@ -69,10 +69,10 @@ def test_remount_blockiert_den_api_start_nicht():
def test_unter_wurzel_faellt_nicht_auf_praefix_namen_herein():
"""Befund 25.07.2026: In main.py prueften neun Stellen mit nacktem
"""Befund 25.07.2026: In main.py prüften neun Stellen mit nacktem
startswith(MEDIA_ROOT) — darunter /browse und /browse/mkdir, wo der Pfad
vom Nutzer kommt. „/app/media-boese/x" beginnt mit „/app/media", liegt
aber ausserhalb. Zwilling von tasks.unter_wurzel im Worker."""
aber außerhalb. Zwilling von tasks.unter_wurzel im Worker."""
from main import unter_wurzel
assert unter_wurzel("/app/media", "/app/media") is True
@@ -87,9 +87,9 @@ def test_unter_wurzel_faellt_nicht_auf_praefix_namen_herein():
def test_ping_vorrat_verhindert_die_wartesekunde(monkeypatch):
"""Gemessen 25.07.2026: /capabilities brauchte 1,010 s - jedes Mal. Der
Celery-Ping sammelt Antworten bis zum Timeout und kann nicht frueher
Celery-Ping sammelt Antworten bis zum Timeout und kann nicht früher
aufhoeren. Fuenf UI-Stellen holen /capabilities, also zahlte jede Seite
eine Sekunde, waehrend alle anderen Endpunkte unter 25 ms lagen.
eine Sekunde, während alle anderen Endpunkte unter 25 ms lagen.
Der Vorrat muss deshalb abgelesen und NICHT neu gepingt werden, solange er
frisch ist - und bei altem Vorrat lieber einmal langsam als falsch.
@@ -119,7 +119,7 @@ def test_ping_vorrat_verhindert_die_wartesekunde(monkeypatch):
def test_ping_takt_ist_kuerzer_als_die_haltbarkeit():
"""Sonst laeuft der Vorrat zwischen zwei Hintergrund-Laeufen ab und der
"""Sonst läuft der Vorrat zwischen zwei Hintergrund-Laeufen ab und der
Endpunkt pingt doch wieder synchron."""
import main
@@ -127,11 +127,11 @@ def test_ping_takt_ist_kuerzer_als_die_haltbarkeit():
def test_tote_routen_sind_und_bleiben_weg():
"""Entfernt am 25.07.2026, jede ein Ueberrest eines ersetzten Entwurfs und
"""Entfernt am 25.07.2026, jede ein Überrest eines ersetzten Entwurfs und
ohne einen einzigen Aufrufer (mechanisch gegengeprueft: alle api.*-Aufrufe
des UI gegen alle Routen).
Der Test haelt sie draussen. /stream/jobs ist der Grund fuer diese
Der Test hält sie draussen. /stream/jobs ist der Grund für diese
Absicherung: er war schon einmal ein Placebo, wurde dann "repariert" statt
entfernt - und war danach eine Endlosschleife je Verbindung ohne jeden
Verbraucher. Wer echtes Push will, braucht BEIDE Seiten (Server UND ein
+11 -11
View File
@@ -1,4 +1,4 @@
"""Tests fuer die puren Helfer aus makemkv_daten.
"""Tests für die puren Helfer aus makemkv_daten.
Bewusst OHNE Dateisystem, DB und fcntl — deshalb laufen sie auch auf Windows
und nicht nur in der Ampel. Geprueft wird genau das, was ohne Container und
@@ -24,8 +24,8 @@ _GUELTIG = """; KEYDB.cfg — Beispiel
def test_zaehle_disc_eintraege_zaehlt_nur_echte_disc_zeilen():
# Kommentar- und Leerzeilen duerfen NICHT mitgezaehlt werden, sonst meldet
# das UI "da liegt was drin", obwohl die Datei keinen Schluessel enthaelt.
# Kommentar- und Leerzeilen dürfen NICHT mitgezaehlt werden, sonst meldet
# das UI "da liegt was drin", obwohl die Datei keinen Schluessel enthält.
assert zaehle_disc_eintraege(_GUELTIG) == 2
@@ -51,8 +51,8 @@ def test_keydb_pruefen_meldet_leere_datei():
def test_keydb_pruefen_erkennt_html():
# Haeufigster Bedienfehler: statt der Datei landet die HTML-Fehlerseite
# eines Downloads im Feld. MakeMKV wuerde dann still weiter meckern.
# Häufigster Bedienfehler: statt der Datei landet die HTML-Fehlerseite
# eines Downloads im Feld. MakeMKV würde dann still weiter meckern.
fehler = keydb_pruefen("<!DOCTYPE html>\n<html><body>404 Not Found</body></html>\n")
assert "HTML" in fehler
@@ -63,19 +63,19 @@ def test_keydb_pruefen_meldet_datei_ohne_disc_zeile():
def test_keydb_pruefen_laesst_gueltige_datei_durch():
# "" heisst laut Vertrag: alles in Ordnung, darf geschrieben werden.
# "" heißt laut Vertrag: alles in Ordnung, darf geschrieben werden.
assert keydb_pruefen(_GUELTIG) == ""
def test_ist_aacs_dump_erkennt_echten_namen():
# So heisst der Dump, den MakeMKV am 25.07.2026 fuer Akira UHD abgelegt hat
# So heißt der Dump, den MakeMKV am 25.07.2026 für Akira UHD abgelegt hat
# (Meldung 3332) — dieser Name MUSS zum Download durchkommen.
assert ist_aacs_dump("MKB20_v76_UHD_AKIRA_C02B.tgz") is True
def test_ist_aacs_dump_blockt_pfad_tricks():
# Der Download-Endpunkt haengt den Namen an das Datenverzeichnis — ein
# durchgelassenes ".." oder ein Pfadtrenner waere ein Ausbruch.
# Der Download-Endpunkt hängt den Namen an das Datenverzeichnis — ein
# durchgelassenes ".." oder ein Pfadtrenner wäre ein Ausbruch.
assert ist_aacs_dump("../x.tgz") is False
assert ist_aacs_dump("../../etc/passwd.tgz") is False
assert ist_aacs_dump("unter/ordner.tgz") is False
@@ -93,8 +93,8 @@ def test_ist_aacs_dump_lehnt_andere_endungen_ab():
def test_settings_conf_ersetzt_alten_key_und_behaelt_den_rest():
# Regression: bis 25.07.2026 wurde die Datei komplett ueberschrieben. Mit
# dem jetzt persistenten Datenverzeichnis waere app_UpdateEnable vor jedem
# Regression: bis 25.07.2026 wurde die Datei komplett überschrieben. Mit
# dem jetzt persistenten Datenverzeichnis wäre app_UpdateEnable vor jedem
# Rip weg gewesen.
alt = 'app_UpdateEnable = "1"\napp_Key = "T-alt"\napp_DestinationDir = "/tmp"\n'
neu = settings_conf_zusammenfuehren(alt, "T-neu")
+3 -3
View File
@@ -1,4 +1,4 @@
"""Tests fuer den MakeMKV-Beta-Key-Parser (dependency-frei, nur extract_key)."""
"""Tests für den MakeMKV-Beta-Key-Parser (dependency-frei, nur extract_key)."""
from makemkv_key import extract_key
# Beispiel-Key im echten Format (T- + 64 Zeichen [A-Za-z0-9_]).
@@ -16,12 +16,12 @@ def test_extract_key_ohne_key_gibt_none():
def test_extract_key_zu_kurz_kein_treffer():
# Format ist streng T- + genau 64 Zeichen — zu kurz faellt raus.
# Format ist streng T- + genau 64 Zeichen — zu kurz fällt raus.
assert extract_key("T-zukurz123") is None
def test_extract_key_liefert_ganzen_key():
# Regression: die Regex darf den Key NICHT abschneiden (Beta-Keys sind laenger
# Regression: die Regex darf den Key NICHT abschneiden (Beta-Keys sind länger
# als die im Forum genannten 64 Zeichen — hier der volle Key zwischen Text).
treffer = extract_key(f"Vorher-Text {_KEY} Nachher-Text")
assert treffer == _KEY