6a17af2118
Ampel / ampel (push) Successful in 28s
Loest die offene Frage aus dem Savepoint ("UHD gar nicht komprimieren?") und
nimmt dem Wizard die Falle, in die er bisher fuehrte.
## Kompression je Disc-Typ abwaehlbar
Bisher gab es nur den globalen Schalter transcodeEnabled: alles komprimieren
oder nichts. Wer 4K verlustfrei behalten und DVDs trotzdem schrumpfen wollte,
hatte keine Moeglichkeit - obwohl genau das die vernuenftige Einstellung fuer
diese Maschine ist (4K-HEVC = gemessene 28-55 h je Film ohne AVX2).
Jetzt kann das Preset eines Disc-Typs auf den Reservewert "keine" stehen, dann
bleibt die verlustfreie Datei aus dem Rip stehen. Neue reine Funktion
komprimieren_fuer(disc_type, einstellungen); der globale Schalter schlaegt
weiter alles. preset_fuer() ueberspringt den Reservewert bewusst und gibt ihn
NIE als Preset-Namen zurueck - sonst bekaeme HandBrake `--preset keine` und
wuerde scheitern. Wer ueber "Neu komprimieren" ausdruecklich doch komprimieren
will, bekommt so ein brauchbares Preset statt eines Fehlers.
Der Reservewert kollidiert mit keinem echten Namen: gegengeprueft gegen alle
90 Presets aus `HandBrakeCLI --preset-list` im Worker-Image. Bei der
Gelegenheit auch die fuenf im UI angebotenen Namen geprueft - alle echt.
## Der Wizard empfiehlt nach GEMESSENER Rechenleistung
Vorher stand H.265 als Standard drin. Auf einer CPU ohne AVX2 sind das ein bis
zwei Tage pro 4K-Film - genau der Lauf, der am 25.07.2026 abgebrochen werden
musste. Der Wizard hatte die Zahlen sogar schon vorliegen (/capabilities
meldet cpu_simd und cpu_kerne), nur benutzt hat er sie nicht.
Jetzt: schwache CPU -> 4K wird nicht komprimiert, Blu-ray/DVD gehen auf H.264
(schneller als H.265). Starke CPU oder Hardware-Encoder -> H.265 durchgehend.
Der Wizard schreibt dabei ALLE vier Preset-Felder, nicht nur das allgemeine -
vorher fiel 4K auf ein 1080p-Preset zurueck und die Aufloesung war weg.
Gewarnt wird nur, wenn es belegt ist: schwacheEncoderCpu() verlangt mindestens
einen Worker mit BEKANNTER SIMD-Stufe und keinen mit Hardware-Encoder. Ein
Windows-Worker meldet "unbekannt" (dort gibt es kein /proc/cpuinfo) - dann wird
geschwiegen statt falsch gewarnt. Auf Windows gegengeprueft: 16 Kerne und
CPU-Modell kommen korrekt durch, encoders ist ohne HandBrake leer.
## Weitere Wizard-Haerten
- Kasten "Was Rippy gerade sieht": Laufwerk, Worker (mit Kernen/SIMD), freier
Platz. Jede Zeile hat bei Problemen eine HANDLUNGSANWEISUNG statt nur eines
Kreuzes - kein Laufwerk, kein Worker und wenig Platz sind die drei Faelle, in
denen man vorher ratlos dastand.
- Laedt alle drei Quellen parallel (Promise.allSettled) und wiederholt im
5-s-Takt: beim ersten Start laeuft der Worker noch hoch, vorher stand dort
dauerhaft "Noch kein Worker gemeldet" ohne Aussicht.
- API-Keys sind SICHTBAR statt als Punkte: das sind kopierte Keys, keine
Passwoerter, und einen Tippfehler sieht man in Punkten nicht.
- Keys werden direkt nach dem Speichern geprueft (/metadata/status). Ein
falsch kopierter Key faellt sofort auf, statt erst beim ersten Rip als
"Unknown Disc" - mit der Wahl "Key korrigieren" oder "Trotzdem fertigstellen".
## Einstellungen -> Verarbeitung
Alle drei Preset-Auswahlen bekommen "Nicht komprimieren", und beim 4K-Feld
erscheint die AVX2-Warnung mit der gemessenen Zahl - aber nur, wenn die
Maschine sie wirklich braucht.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
320 lines
14 KiB
Python
320 lines
14 KiB
Python
"""Tests für ripping.py: Kommando-Bau und Fortschritts-Parsing.
|
|
|
|
Deckt genau die Stellen ab, an denen Reviews erfundene Schnittstellen fanden
|
|
(abcde-Flags, Celery-API, HandBrake-Regex) — damit so etwas nie wieder still liegt.
|
|
Das PRGV-Format stammt aus der MakeMKV-Doku (makemkv.com/developers/usage.txt).
|
|
"""
|
|
|
|
import os
|
|
|
|
from ripping import (
|
|
build_abcde_cmd,
|
|
build_handbrake_cmd,
|
|
build_makemkv_cmd,
|
|
get_progress_from_line,
|
|
get_progress_from_prgv,
|
|
parse_msg,
|
|
write_abcde_config,
|
|
)
|
|
|
|
|
|
def test_makemkv_cmd_vollstaendig():
|
|
cmd = build_makemkv_cmd("/dev/sr0", "/app/media/dvd/x")
|
|
assert cmd[0] == "makemkvcon"
|
|
assert "-r" in cmd # Robot-Mode: maschinenlesbar
|
|
assert "--noscan" in cmd # Scan hängt/crasht im Container (23.07.)
|
|
assert "--progress=-same" in cmd # Fortschritt im selben Stream
|
|
assert "mkv" in cmd
|
|
assert "dev:/dev/sr0" in cmd # Geräte-Notation laut Doku
|
|
assert cmd[-2:] == ["all", "/app/media/dvd/x"]
|
|
|
|
|
|
def test_handbrake_cmd_arbeitet_auf_datei_nicht_geraet():
|
|
"""HandBrake kann AACS nicht — es bekommt IMMER die MKV-Datei aus dem
|
|
MakeMKV-Rip, nie das Laufwerk (die alte Direkt-am-Gerät-Pipeline war
|
|
für Blu-rays prinzipiell funktionsunfähig)."""
|
|
cmd = build_handbrake_cmd("/app/temp/raw/x/t00.mkv", "/app/media/bluray/x/t00.mkv")
|
|
assert cmd[0] == "HandBrakeCLI"
|
|
assert cmd[cmd.index("--input") + 1] == "/app/temp/raw/x/t00.mkv"
|
|
assert cmd[cmd.index("--output") + 1] == "/app/media/bluray/x/t00.mkv"
|
|
assert "--preset" in cmd
|
|
assert "--all-audio" in cmd # alle Sprachen behalten
|
|
assert "--all-subtitles" in cmd
|
|
|
|
|
|
def test_handbrake_progress_parsing():
|
|
# Testfund 22.07.: echtes HandBrake schreibt 45.50 % MIT Leerzeichen
|
|
assert get_progress_from_line("Encoding: task 1 of 1, 45.50 %") == 45
|
|
assert get_progress_from_line("Encoding: task 1 of 1, 100.00 %") == 100
|
|
# Echte Zeile mit fps-Anhang, wie sie im Binary steht
|
|
assert get_progress_from_line(
|
|
"Encoding: task 1 of 1, 12.34 % (5.67 fps, avg 4.32 fps, ETA 00h12m34s)"
|
|
) == 12
|
|
# Ein echtes 0 % ist eine ANGABE, keine Leermeldung
|
|
assert get_progress_from_line("Encoding: task 1 of 1, 0.00 %") == 0
|
|
|
|
|
|
def test_handbrake_progress_ignoriert_scan_durchlauf():
|
|
"""Befund 25.07.2026 (Akira-UHD, live gemessen): HandBrake läuft VOR dem
|
|
Encodieren einen Scan-Durchlauf, der ebenfalls Prozente ausgibt und dabei
|
|
bis 100 % steigt. Die alte Regex nahm jede Zahl vor einem Prozentzeichen
|
|
und meldete deshalb 99 %, während der Encode bei 1 % stand.
|
|
|
|
-1 heißt „keine Encode-Fortschrittszeile" — dasselbe Muster wie bei
|
|
get_progress_from_prgv. Fremde Zeilen dürfen NIE als 0 % gelten.
|
|
"""
|
|
assert get_progress_from_line("Scanning title 1 of 1, preview 3, 30.00 %") == -1
|
|
assert get_progress_from_line("Scanning title 1 of 1, preview 10, 100.00 %") == -1
|
|
# Vorlauf-Phase: Prozente beziehen sich auf die Suche, nicht auf den Encode
|
|
assert get_progress_from_line(
|
|
"Encoding: task 1 of 1, Searching for start time, 42.00 %"
|
|
) == -1
|
|
assert get_progress_from_line("kein Fortschritt hier") == -1
|
|
assert get_progress_from_line("Muxing: this may take awhile...") == -1
|
|
assert get_progress_from_line("") == -1
|
|
|
|
|
|
def test_handbrake_progress_rechnet_zwei_durchlaeufe_zusammen():
|
|
"""Presets mit zwei Durchläufen zählen die Prozente je Durchlauf neu.
|
|
Ohne Verrechnung sprang die Anzeige mitten in der Datei zurück auf 0."""
|
|
assert get_progress_from_line("Encoding: task 1 of 2, 50.00 %") == 25
|
|
assert get_progress_from_line("Encoding: task 2 of 2, 0.00 %") == 50
|
|
assert get_progress_from_line("Encoding: task 2 of 2, 100.00 %") == 100
|
|
|
|
|
|
def test_prgv_parsing():
|
|
# PRGV:current,total,max — total/max ist der Gesamtfortschritt
|
|
assert get_progress_from_prgv("PRGV:100,32768,65536") == 50
|
|
assert get_progress_from_prgv("PRGV:0,65536,65536") == 100
|
|
assert get_progress_from_prgv("PRGV:0,0,65536") == 0
|
|
|
|
|
|
def test_prgv_parsing_ignoriert_fremde_zeilen():
|
|
# -1 heißt „keine Fortschrittszeile" — MSG-Zeilen dürfen NIE als 0% gelten,
|
|
# sonst springt die Anzeige ständig auf null zurück.
|
|
assert get_progress_from_prgv('MSG:1005,0,1,"MakeMKV gestartet","%1","x"') == -1
|
|
assert get_progress_from_prgv("irgendwas") == -1
|
|
assert get_progress_from_prgv("PRGV:kaputt") == -1
|
|
|
|
|
|
def test_parse_msg_trennt_code_und_klartext():
|
|
"""Echte Zeilen aus einem makemkvcon-Lauf vom 25.07.2026 (Akira UHD).
|
|
|
|
Feld 4 ist laut https://www.makemkv.com/developers/usage.txt der fertig
|
|
zusammengesetzte Klartext — genau der landet im Rippy-Log.
|
|
"""
|
|
assert parse_msg(
|
|
'MSG:1005,0,1,"MakeMKV v1.18.4 linux(x64-release) started","%1 started","MakeMKV v1.18.4 linux(x64-release)"'
|
|
) == (1005, "MakeMKV v1.18.4 linux(x64-release) started")
|
|
assert parse_msg(
|
|
'MSG:1011,0,1,"Using LibreDrive mode (v06.3 id=866A98CB9C4E)","%1","Using LibreDrive mode (v06.3 id=866A98CB9C4E)"'
|
|
) == (1011, "Using LibreDrive mode (v06.3 id=866A98CB9C4E)")
|
|
|
|
|
|
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
|
|
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"'
|
|
) == (3303, "The volume key is unknown for this disc - video can't be decrypted")
|
|
assert parse_msg('MSG:5010,0,0,"Failed to open disc","Failed to open disc"') == (
|
|
5010,
|
|
"Failed to open disc",
|
|
)
|
|
|
|
|
|
def test_parse_msg_schneidet_meldungen_mit_komma_nicht_ab():
|
|
"""DER Grund fuer 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
|
|
ein Satzfragment, das mehr verwirrt als hilft."""
|
|
zeile = (
|
|
'MSG:3025,0,3,"Title #1 has length of 12 seconds, which is less than '
|
|
'minimum title length of 120 seconds and was therefore skipped",'
|
|
'"Title #%1 has length of %2 seconds which is less than minimum title '
|
|
'length of %3 seconds and was therefore skipped","1","12","120"'
|
|
)
|
|
code, text = parse_msg(zeile)
|
|
assert code == 3025
|
|
assert text.endswith("and was therefore skipped")
|
|
assert "which is less than" in text
|
|
|
|
|
|
def test_parse_msg_ignoriert_fremde_zeilen():
|
|
# Alles ausser 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
|
|
assert parse_msg("TCOUNT:5") is None
|
|
assert parse_msg("") is None
|
|
assert parse_msg("irgendwelcher Muell ohne Struktur") is None
|
|
|
|
|
|
def test_abcde_cmd_hat_genau_ein_ausgabeformat():
|
|
"""Review-Fund 22.07.: '-o' stand doppelt (Format UND Verzeichnis) — abcde
|
|
parste das Verzeichnis als Format, CD-Ripping war nie funktionsfähig."""
|
|
cmd = build_abcde_cmd("/dev/sr0", "/tmp/test.abcde.conf")
|
|
assert cmd.count("-o") == 1
|
|
assert cmd[cmd.index("-o") + 1] == "flac"
|
|
assert "-c" in cmd
|
|
assert cmd[cmd.index("-c") + 1] == "/tmp/test.abcde.conf"
|
|
assert "-N" in cmd # nicht-interaktiv, sonst hängt der Worker
|
|
|
|
|
|
def test_abcde_auswurf_folgt_der_einstellung():
|
|
"""Befund 25.07.2026: `-x` (Auswurf) stand fest verdrahtet drin. Eine
|
|
Audio-CD warf damit IMMER aus, eine DVD/Blu-ray NIE — und die Einstellung
|
|
„Automatischer Auswurf" regelte keines von beidem, weil sie nirgends
|
|
gelesen wurde."""
|
|
assert "-x" in build_abcde_cmd("/dev/sr0", "/tmp/c.conf", auswerfen=True)
|
|
assert "-x" not in build_abcde_cmd("/dev/sr0", "/tmp/c.conf", auswerfen=False)
|
|
# Standard bleibt „auswerfen" — so war das Verhalten bisher
|
|
assert "-x" in build_abcde_cmd("/dev/sr0", "/tmp/c.conf")
|
|
# Die Config darf durch das weggefallene -x nicht verrutschen
|
|
ohne = build_abcde_cmd("/dev/sr0", "/tmp/c.conf", auswerfen=False)
|
|
assert ohne[ohne.index("-c") + 1] == "/tmp/c.conf"
|
|
|
|
|
|
def test_abcde_config_enthaelt_zielverzeichnis():
|
|
pfad = write_abcde_config("/app/media/cd/test123")
|
|
try:
|
|
with open(pfad, encoding="utf-8") as f:
|
|
inhalt = f.read()
|
|
assert "OUTPUTDIR='/app/media/cd/test123'" in inhalt
|
|
assert "INTERACTIVE=n" in inhalt
|
|
finally:
|
|
os.unlink(pfad)
|
|
|
|
|
|
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
|
|
# die 4K-Aufloesung still verlorengegangen.
|
|
from ripping import preset_fuer
|
|
|
|
einstellungen = {
|
|
"transcodePreset": "HQ 1080p30 Surround",
|
|
"transcodePresetDvd": "H.265 MKV 576p25",
|
|
"transcodePresetBluray": "H.265 MKV 1080p30",
|
|
"transcodePresetUhd": "H.265 MKV 2160p60 4K",
|
|
}
|
|
assert preset_fuer("uhd", einstellungen) == "H.265 MKV 2160p60 4K"
|
|
assert preset_fuer("bluray", einstellungen) == "H.265 MKV 1080p30"
|
|
assert preset_fuer("dvd", einstellungen) == "H.265 MKV 576p25"
|
|
|
|
|
|
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.
|
|
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)
|
|
assert preset_fuer("uhd", {"transcodePresetUhd": " ", "transcodePreset": "X"}) == "X"
|
|
|
|
|
|
def test_preset_fuer_ohne_einstellungen_nimmt_den_eingebauten_standard():
|
|
# Unbekannter Disc-Typ, leere oder fehlende Einstellungen: nie None, nie
|
|
# Absturz — sonst stirbt die Kompression an einem leeren --preset-Argument.
|
|
from ripping import DEFAULT_HB_PRESET, preset_fuer
|
|
|
|
assert preset_fuer("uhd", {}) == DEFAULT_HB_PRESET
|
|
assert preset_fuer("", None) == DEFAULT_HB_PRESET
|
|
assert preset_fuer(None, {}) == DEFAULT_HB_PRESET
|
|
assert preset_fuer("cd", {"transcodePresetUhd": "egal"}) == DEFAULT_HB_PRESET
|
|
|
|
|
|
def test_handbrake_prueft_abbruch_bei_jeder_zeile_nicht_nur_bei_fortschritt():
|
|
"""Befund 25.07.2026 (am laufenden Akira-Job beobachtet): Der Abbruch hing
|
|
am Fortschritts-Callback, und der stieg bei unveraenderter Prozentzahl
|
|
sofort aus. Bei einem 4K-Encode mit einem Prozent je halber Stunde sah
|
|
„Abbrechen" minutenlang wirkungslos aus (gemessen: 3,4 min).
|
|
|
|
Der Abbruch-Kanal muss deshalb JEDE Ausgabezeile sehen — auch die des
|
|
Scan-Durchlaufs, der gar keine Encode-Prozente liefert.
|
|
"""
|
|
import ripping
|
|
|
|
zeilen = [
|
|
"Scanning title 1 of 1, preview 1, 10.00 %\n",
|
|
"Scanning title 1 of 1, preview 2, 20.00 %\n",
|
|
"Encoding: task 1 of 1, 0.00 %\n",
|
|
"Encoding: task 1 of 1, 0.00 %\n",
|
|
]
|
|
gesehen = []
|
|
|
|
class FakeProcess:
|
|
def __init__(self):
|
|
self.stdout = iter(zeilen)
|
|
self.returncode = 0
|
|
self.getoetet = False
|
|
|
|
def kill(self):
|
|
self.getoetet = True
|
|
|
|
def wait(self):
|
|
return 0
|
|
|
|
prozess = FakeProcess()
|
|
|
|
def abbruch_cb():
|
|
gesehen.append(1)
|
|
if len(gesehen) == 2: # beim zweiten Mal abbrechen
|
|
raise ripping.RipAbbruch()
|
|
|
|
ergebnis = ripping._handbrake_schleife(prozess, "/x.mkv", abbruch_cb, None)
|
|
|
|
assert ergebnis["status"] == "cancelled"
|
|
assert prozess.getoetet is True
|
|
# Zwei Scan-Zeilen genuegten — es musste NICHT auf eine Encode-Zeile gewartet
|
|
# werden. Genau das war der Fehler.
|
|
assert len(gesehen) == 2
|
|
|
|
|
|
def test_komprimieren_fuer_globaler_schalter_schlaegt_alles():
|
|
from ripping import komprimieren_fuer
|
|
|
|
assert komprimieren_fuer("uhd", {"transcodeEnabled": False}) is False
|
|
assert komprimieren_fuer("dvd", {"transcodeEnabled": False}) is False
|
|
# Standard ist AN - Bestandsinstallationen aendern 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
|
|
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."""
|
|
from ripping import PRESET_KEINE, komprimieren_fuer
|
|
|
|
nur_uhd_aus = {"transcodePresetUhd": PRESET_KEINE,
|
|
"transcodePresetDvd": "H.265 MKV 576p25"}
|
|
assert komprimieren_fuer("uhd", nur_uhd_aus) is False
|
|
assert komprimieren_fuer("dvd", nur_uhd_aus) is True
|
|
assert komprimieren_fuer("bluray", nur_uhd_aus) is True
|
|
|
|
|
|
def test_preset_fuer_gibt_den_reservewert_nie_als_preset_zurueck():
|
|
"""Sonst bekaeme HandBrake `--preset keine` und wuerde scheitern. Wer ueber
|
|
"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
|
|
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."""
|
|
from ripping import PRESET_KEINE
|
|
|
|
assert PRESET_KEINE == "keine"
|
|
assert " " not in PRESET_KEINE # echte Preset-Namen haben durchweg Leerzeichen
|