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:
+84
-16
@@ -25,6 +25,36 @@ class RipAbbruch(Exception):
|
||||
Nutzer den Job abgebrochen hat (Status 'canceling' in der DB)."""
|
||||
|
||||
|
||||
# include/uapi/linux/cdrom.h — dasselbe ioctl wie in api/devices.py
|
||||
CDROMEJECT = 0x5309
|
||||
|
||||
|
||||
def wirf_disc_aus(device_path: str) -> bool:
|
||||
"""Wirft die Disc aus; gibt zurück, ob es geklappt hat. Wirft NIE.
|
||||
|
||||
Befund 25.07.2026: Die Einstellung „Automatischer Auswurf" (Standard: ein,
|
||||
Beschreibung „Disc nach erfolgreichem Ripping automatisch auswerfen") wurde
|
||||
von niemandem gelesen — bei DVD/Blu-ray warf Rippy nie aus.
|
||||
|
||||
Bewusst hier und nicht in detection.py: das Modul ist ein byteweiser
|
||||
Zwilling der API-Kopie. `fcntl` gibt es nur unter Linux — der native
|
||||
Windows-Worker lädt ripping.py ebenfalls, rippt dort aber nie.
|
||||
"""
|
||||
try:
|
||||
from fcntl import ioctl
|
||||
except ImportError: # Windows
|
||||
return False
|
||||
try:
|
||||
fd = os.open(device_path, os.O_RDONLY | os.O_NONBLOCK)
|
||||
try:
|
||||
ioctl(fd, CDROMEJECT, 0)
|
||||
finally:
|
||||
os.close(fd)
|
||||
return True
|
||||
except OSError:
|
||||
return False
|
||||
|
||||
|
||||
def check_makemkv_installed() -> bool:
|
||||
"""Prüft, ob makemkvcon installiert ist."""
|
||||
return shutil.which("makemkvcon") is not None
|
||||
@@ -271,16 +301,42 @@ def build_handbrake_cmd(input_path: str, output_path: str, preset: str = DEFAULT
|
||||
]
|
||||
|
||||
|
||||
def get_progress_from_line(line: str) -> int:
|
||||
"""Extrahiert Fortschritt in Prozent aus HandBrake-Ausgabe.
|
||||
HB_ENCODE_RE = re.compile(
|
||||
r'^Encoding:\s*task\s+(\d+)\s+of\s+(\d+),\s*(\d+(?:\.\d+)?)\s*%'
|
||||
)
|
||||
|
||||
Testfund 22.07.: echtes HandBrake schreibt „45.50 %" MIT Leerzeichen vor
|
||||
dem Prozentzeichen — eine Regex ohne \\s* parst NIE echte Ausgabe.
|
||||
|
||||
def get_progress_from_line(line: str) -> int:
|
||||
"""Extrahiert den ENCODE-Fortschritt in Prozent aus HandBrake-Ausgabe.
|
||||
|
||||
Gibt **-1** zurück, wenn die Zeile kein Encode-Fortschritt ist — genau wie
|
||||
get_progress_from_prgv. Sonst wäre ein echtes „0 %" nicht von „keine
|
||||
Angabe" zu unterscheiden, und der Anfang des Encodes bliebe unsichtbar.
|
||||
|
||||
Formatstrings aus dem HandBrakeCLI-Binary im Worker-Image gelesen
|
||||
(AGENTS Regel D — nicht aus dem Kopf):
|
||||
Scanning title %d of %d, preview %d, %.2f %% → Scan, NICHT Encode
|
||||
Encoding: task %d of %d, %.2f %% (%.2f fps, avg → Encode
|
||||
Encoding: task %d of %d, Searching for start time, %.2f → Vorlauf, NICHT Encode
|
||||
|
||||
Befund 25.07.2026 (Akira-UHD, live gemessen): Die alte Regex nahm JEDE
|
||||
Zahl vor einem Prozentzeichen — also auch den Scan-Durchlauf, der VOR dem
|
||||
Encodieren einmal bis 100 % hochläuft. Die Anzeige stand deshalb auf 99 %,
|
||||
während der eigentliche Encode bei 1 % war. Zusammen mit dem alten Filter
|
||||
`progress > 0` im Aufrufer blieb sie dort stundenlang kleben, weil
|
||||
int(0.87) == 0 verworfen wurde.
|
||||
|
||||
`task N of M` wird mitgerechnet: Presets mit zwei Durchläufen zählen die
|
||||
Prozente je Durchlauf neu, sonst sprang die Anzeige mitten in der Datei
|
||||
zurück auf 0.
|
||||
"""
|
||||
match = re.search(r'(\d+\.\d+)\s*%', line)
|
||||
if match:
|
||||
return int(float(match.group(1)))
|
||||
return 0
|
||||
match = HB_ENCODE_RE.match(line.strip())
|
||||
if not match:
|
||||
return -1
|
||||
aktuell, gesamt, prozent = int(match.group(1)), int(match.group(2)), float(match.group(3))
|
||||
if gesamt < 1 or aktuell < 1:
|
||||
return -1
|
||||
return int(((aktuell - 1) * 100 + prozent) / gesamt)
|
||||
|
||||
|
||||
def run_handbrake(input_path: str, output_path: str, preset: str = DEFAULT_HB_PRESET, progress_cb=None) -> dict:
|
||||
@@ -300,7 +356,10 @@ def run_handbrake(input_path: str, output_path: str, preset: str = DEFAULT_HB_PR
|
||||
try:
|
||||
for line in process.stdout:
|
||||
progress = get_progress_from_line(line)
|
||||
if progress > 0 and progress_cb:
|
||||
# >= 0: ein echtes 0 % ist eine Angabe und muss durch. Der alte
|
||||
# Filter `> 0` verwarf den gesamten ersten Prozentpunkt — bei
|
||||
# einem 4K-Encode sind das Stunden ohne sichtbare Bewegung.
|
||||
if progress >= 0 and progress_cb:
|
||||
progress_cb(progress)
|
||||
except RipAbbruch:
|
||||
process.kill()
|
||||
@@ -320,20 +379,27 @@ def run_handbrake(input_path: str, output_path: str, preset: str = DEFAULT_HB_PR
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def build_abcde_cmd(device_path: str, config_path: str) -> list:
|
||||
def build_abcde_cmd(device_path: str, config_path: str, auswerfen: bool = True) -> list:
|
||||
"""Baut das abcde-Kommando (pure Funktion, testbar).
|
||||
|
||||
-o = Ausgabeformat (flac), -N = nicht-interaktiv, -x = Eject am Ende,
|
||||
-c = Config-Datei (enthält OUTPUTDIR). NIE ein Verzeichnis an -o geben.
|
||||
|
||||
Befund 25.07.2026: `-x` stand fest verdrahtet drin. Damit warf eine
|
||||
Audio-CD IMMER aus, während eine DVD/Blu-ray NIE auswarf — und die
|
||||
Einstellung „Automatischer Auswurf" regelte keines von beidem, weil sie
|
||||
nirgends gelesen wurde. Jetzt entscheidet sie hier mit.
|
||||
"""
|
||||
return [
|
||||
cmd = [
|
||||
"abcde",
|
||||
"-d", device_path,
|
||||
"-o", "flac",
|
||||
"-N",
|
||||
"-x",
|
||||
"-c", config_path
|
||||
]
|
||||
if auswerfen:
|
||||
cmd.append("-x")
|
||||
cmd += ["-c", config_path]
|
||||
return cmd
|
||||
|
||||
|
||||
def write_abcde_config(output_dir: str) -> str:
|
||||
@@ -467,8 +533,10 @@ def rip_video(device_path: str, disc_id: str, disc_type: str = "dvd", progress_c
|
||||
log_cb=log_cb)
|
||||
|
||||
|
||||
def rip_cd(device_path: str, disc_id: str, progress_cb=None, output_dir: str = None) -> dict:
|
||||
"""Rippt eine CD mit abcde (FLAC)."""
|
||||
def rip_cd(device_path: str, disc_id: str, progress_cb=None, output_dir: str = None,
|
||||
auswerfen: bool = True) -> dict:
|
||||
"""Rippt eine CD mit abcde (FLAC). `auswerfen` = Einstellung „Automatischer
|
||||
Auswurf" (abcde macht das selbst per -x)."""
|
||||
if not check_abcde_installed():
|
||||
return {
|
||||
"status": "error",
|
||||
@@ -492,7 +560,7 @@ def rip_cd(device_path: str, disc_id: str, progress_cb=None, output_dir: str = N
|
||||
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
build_abcde_cmd(device_path, config_path),
|
||||
build_abcde_cmd(device_path, config_path, auswerfen=auswerfen),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
|
||||
Reference in New Issue
Block a user