fix(worker,api): Platten-Schutz griff nicht, Fortschritt log, Auswurf tat nichts

Vier Funde aus der Durchsicht, alle auf der VM gemessen.

1. DER PLATTEN-SCHUTZ AUS c065967 WAR WIRKUNGSLOS

_original_aufheben() entschied per os.stat().st_dev, ob umgehaengt oder
kopiert werden muss. Im Worker-Container gemessen - beides gleichzeitig wahr:

    st_dev /app/temp  = 2050
    st_dev /app/media = 2050        → identisch
    os.rename(...)    → EXDEV, "Invalid cross-device link"

Der Kernel vergleicht bei rename() den MOUNT, nicht das Geraet. /app/temp
(Docker-Volume) und /app/media (Bind-Mount) sind zwei Mounts DERSELBEN
ext4-Partition. Die Pruefung sah "gleiches Dateisystem", uebersprang die
Platzpruefung, und shutil.move kopierte doch - 75 GB bei 37 GB frei. Der
Schutz haette genau den Schaden zugelassen, gegen den er gebaut wurde.

Jetzt wird os.rename VERSUCHT statt vorhergesagt: klappt es, ist es
umgehaengt und fertig; kommt EXDEV, steht die Kopie fest und ERST DANN wird
der Platz geprueft. Das ist keine Vermutung mehr, sondern die Antwort des
Kernels. Vier Tests in test_original_aufheben.py, darunter genau der Fall,
der die Platte fuellte. Die zwei alten Tests in test_medien.py sind dorthin
gewandert - sie taeuschten per gefaelschtem os.stat "verschiedene
Dateisysteme" vor, also genau die Annahme, an der der Schutz scheiterte.

2. DIE FORTSCHRITTSANZEIGE ZEIGTE DEN SCAN, NICHT DEN ENCODE

get_progress_from_line matchte jede Zahl vor einem Prozentzeichen. HandBrake
gibt Prozente aber in drei Phasen aus (Formatstrings aus dem Binary gelesen):

    Scanning title %d of %d, preview %d, %.2f %%          → laeuft VOR dem
                                                            Encode bis 100 %
    Encoding: task %d of %d, %.2f %%       (%.2f fps, avg  → der echte Wert
    Encoding: task %d of %d, Searching for start time, ... → Vorlauf

Dazu warf `if progress > 0` im Aufrufer jeden Wert unter 1,00 % weg. Live
beobachtet: Anzeige stand auf 99 %, der Encode bei 1,06 %; sie fiel erst auf
1, als der Encode die 1-%-Marke ueberschritt. Jetzt wird nur die
Encoding-Zeile gelesen, `task N of M` mitgerechnet (sonst springt die
Anzeige bei Zwei-Pass-Presets mitten in der Datei zurueck), und -1 heisst
"keine Angabe" - dasselbe Muster wie bei get_progress_from_prgv.

3. "AUTOMATISCHER AUSWURF" WURDE VON NIEMANDEM GELESEN

Die Einstellung (Standard: ein, "Disc nach erfolgreichem Ripping automatisch
auswerfen") kam in keiner Zeile Backend-Code vor. DVD/Blu-ray warfen deshalb
NIE aus, Audio-CDs IMMER, weil abcde `-x` fest verdrahtet bekam. Jetzt
entscheidet die Einstellung beides: wirf_disc_aus() per CDROMEJECT-ioctl
(fcntl-guarded, der native Windows-Worker laedt das Modul auch) und `-x` nur
noch, wenn gewuenscht.

4. PFAD-PRUEFUNG FIEL AUF PRAEFIX-NAMEN HEREIN

Elf Stellen prueften mit nacktem startswith(MEDIA_ROOT). "/app/media-boese/x"
beginnt mit "/app/media", liegt aber ausserhalb - betroffen waren auch
/browse und /browse/mkdir, wo der Pfad vom Nutzer kommt. Neuer
Zwillings-Helfer unter_wurzel() in api/main.py und worker/tasks.py, alle elf
Stellen umgestellt, Tests in beiden.

Nebenbefund: _zielbasis() benutzte os.path.normpath - unter Windows werden
daraus Backslashes, die MEDIA_ROOT-Pruefung greift nicht mehr, und das
gewaehlte Ziel faellt still auf den Standard zurueck. Genau die Falle, die
_arbeitsverzeichnis() drei Zeilen weiter dokumentiert und mit posixpath
vermeidet. Live war es nie (nur aus rip_disc, das auf Windows verriegelt
ist), jetzt konsistent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-25 21:05:11 +02:00
parent 883c1c290b
commit ef0a574a70
7 changed files with 455 additions and 116 deletions
+49 -2
View File
@@ -43,10 +43,43 @@ def test_handbrake_cmd_arbeitet_auf_datei_nicht_geraet():
def test_handbrake_progress_parsing():
# Testfund 22.07.: echtes HandBrake schreibt 45.50 %" MIT Leerzeichen
# 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
assert get_progress_from_line("kein Fortschritt hier") == 0
# 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():
@@ -129,6 +162,20 @@ def test_abcde_cmd_hat_genau_ein_ausgabeformat():
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: