ad48276fdd
Ampel / ampel (push) Failing after 14m47s
Echtes HandBrake schreibt "45.50 %" mit Leerzeichen - die alte Regex hat aus echter Ausgabe NIE einen Fortschritt geparst. Der neue Test mit realistischer Zeile hat es sofort gefangen.
227 lines
7.0 KiB
Python
227 lines
7.0 KiB
Python
"""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
|