"""Ripping-Tasks: DVD/Blu-ray via HandBrake, CD via abcde. Review-Fixes 22.07.2026: - Fortschritt läuft jetzt über Celery `update_state` (Standard) — vorher gingen send_task-Aufrufe an einen Task `update_progress`, den es NIE gab. - abcde-Kommando korrigiert: `-o` ist das AUSGABEFORMAT (nicht das Verzeichnis!), das Zielverzeichnis geht als OUTPUTDIR über eine Config-Datei (-c). Vorher wurde das Verzeichnis als Format geparst — CD-Ripping war nie funktionsfähig. """ import os import re import shutil import subprocess import tempfile from celery_app import celery_app HANDBRAKE_PRESET = "Fast 1080p30" def check_handbrake_installed() -> bool: """Prüft, ob HandBrakeCLI installiert ist.""" return shutil.which("HandBrakeCLI") is not None def check_abcde_installed() -> bool: """Prüft, ob abcde installiert ist.""" return shutil.which("abcde") is not None def check_cdparanoia_installed() -> bool: """Prüft, ob cdparanoia installiert ist.""" return shutil.which("cdparanoia") is not None def get_progress_from_line(line: str) -> int: """Extrahiert Fortschritt in Prozent aus HandBrake-Ausgabe. Testfund 22.07.: echtes HandBrake schreibt „45.50 %" MIT Leerzeichen vor dem Prozentzeichen — die alte Regex ohne \\s* hat NIE einen Fortschritt aus echter Ausgabe geparst. """ match = re.search(r'(\d+\.\d+)\s*%', line) if match: return int(float(match.group(1))) return 0 def build_handbrake_cmd(device_path: str, output_path: str) -> list: """Baut das HandBrake-Kommando (pure Funktion, testbar).""" return [ "HandBrakeCLI", "--input", device_path, "--output", output_path, "--all", "--progress", "--preset", HANDBRAKE_PRESET ] def build_abcde_cmd(device_path: str, config_path: str) -> 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. """ return [ "abcde", "-d", device_path, "-o", "flac", "-N", "-x", "-c", config_path ] def write_abcde_config(output_dir: str) -> str: """Schreibt eine minimale abcde-Config mit dem Zielverzeichnis, gibt Pfad zurück.""" tmp = tempfile.NamedTemporaryFile( "w", suffix=".abcde.conf", delete=False, encoding="utf-8" ) tmp.write(f"OUTPUTDIR='{output_dir}'\nINTERACTIVE=n\n") tmp.close() return tmp.name def run_handbrake(device_path: str, output_path: str, progress_cb=None) -> dict: """Rippt eine DVD/Blu-ray mit HandBrakeCLI; meldet Fortschritt via Callback.""" if not check_handbrake_installed(): return {"status": "error", "error": "HandBrakeCLI ist nicht installiert"} try: process = subprocess.Popen( build_handbrake_cmd(device_path, output_path), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1 ) for line in process.stdout: progress = get_progress_from_line(line) if progress > 0 and progress_cb: progress_cb(progress) process.wait() if process.returncode == 0: return { "status": "success", "output_path": output_path, "return_code": process.returncode } return { "status": "error", "error": f"HandBrake failed with code {process.returncode}", "return_code": process.returncode } except Exception as e: return {"status": "error", "error": str(e)} def detect_disc_type(device_path: str) -> str: """Erkennt den Disc-Typ anhand des Gerätepfads.""" try: result = subprocess.run( ["file", "-L", device_path], capture_output=True, text=True, timeout=10 ) output = result.stdout.lower() if "dvd" in output or "video_ts" in output: return "dvd" elif "bluray" in output or "bdmv" in output: return "bluray" elif "audio" in output or "cda" in output: return "cd" return "unknown" except Exception: return "unknown" def _progress_melder(task): """Baut einen Fortschritts-Callback, der Celery-Standard update_state nutzt.""" def melde(progress: int, message: str = ""): task.update_state( state="PROGRESS", meta={"progress": progress, "status": "ripping", "message": message} ) return melde @celery_app.task(bind=True, name="worker.ripping.rip_dvd") def rip_dvd(self, device_path: str, disc_id: str) -> dict: """Rippt eine DVD mit HandBrake.""" output_dir = f"/output/dvd/{disc_id}" os.makedirs(output_dir, exist_ok=True) output_path = f"{output_dir}/dvd_{disc_id}.mkv" melde = _progress_melder(self) return run_handbrake(device_path, output_path, progress_cb=melde) @celery_app.task(bind=True, name="worker.ripping.rip_bluray") def rip_bluray(self, device_path: str, disc_id: str) -> dict: """Rippt eine Blu-ray mit HandBrake.""" output_dir = f"/output/bluray/{disc_id}" os.makedirs(output_dir, exist_ok=True) output_path = f"{output_dir}/bluray_{disc_id}.mkv" melde = _progress_melder(self) return run_handbrake(device_path, output_path, progress_cb=melde) @celery_app.task(bind=True, name="worker.ripping.rip_cd") def rip_cd(self, device_path: str, disc_id: str) -> dict: """Rippt eine CD mit abcde (FLAC).""" if not check_abcde_installed(): return { "status": "error", "error": "abcde ist nicht installiert. Installiere abcde und cdparanoia für CD-Ripping." } if not check_cdparanoia_installed(): return { "status": "error", "error": "cdparanoia ist nicht installiert. Installiere abcde und cdparanoia für CD-Ripping." } output_dir = f"/output/cd/{disc_id}" os.makedirs(output_dir, exist_ok=True) melde = _progress_melder(self) config_path = write_abcde_config(output_dir) try: process = subprocess.Popen( build_abcde_cmd(device_path, config_path), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, bufsize=1 ) for line in process.stdout: melde(50, line.strip()[:200]) process.wait() if process.returncode == 0: melde(90, "MusicBrainz Lookup abgeschlossen") return { "status": "success", "output_dir": output_dir, "return_code": process.returncode } return { "status": "error", "error": f"abcde failed with code {process.returncode}", "return_code": process.returncode } except Exception as e: return {"status": "error", "error": str(e)} finally: try: os.unlink(config_path) except OSError: pass