- Metadaten-Preview WIEDERHERGESTELLT (Stub ueberschattete echte prescan-Implementierung), tote Altmodule geloescht - Celery update_state statt Phantom-Task/erfundener API - abcde-Kommando korrigiert (CD-Ripping war nie funktionsfaehig) - JWT: fester Schluessel Pflicht, echtes Logout, Cleanup nur Abgelaufene - main.py: crashende Endpoints (Path/secrets/api_keys), year-Bug, Admin-Login aus .env - Ruff gruen (29 Funde), Tests: auth/cache_keys/ripping_helpers, Placebo-test_health raus - SAVEPOINT: offene MakeMKV-Entscheidung SICHTBAR gemacht (Regel B)
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
"""Pytest-Setup für Worker-Tests: flache Modul-Imports."""
|
||||
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(__file__))
|
||||
+180
-184
@@ -1,108 +1,29 @@
|
||||
"""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 subprocess
|
||||
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 get_progress_from_line(line: str) -> int:
|
||||
"""Extrahiert Fortschritt in Prozent aus HandBrake-Ausgabe."""
|
||||
match = re.search(r'(\d+\.\d+)%', line)
|
||||
if match:
|
||||
return int(float(match.group(1)))
|
||||
return 0
|
||||
|
||||
|
||||
def run_handbrake(device_path: str, output_path: str) -> dict:
|
||||
"""Rippt eine DVD/Blu-ray mit HandBrakeCLI."""
|
||||
if not check_handbrake_installed():
|
||||
return {
|
||||
"status": "error",
|
||||
"error": "HandBrakeCLI ist nicht installiert"
|
||||
}
|
||||
|
||||
try:
|
||||
cmd = [
|
||||
"HandBrakeCLI",
|
||||
"--input", device_path,
|
||||
"--output", output_path,
|
||||
"--all",
|
||||
"--progress",
|
||||
"--preset", "Fast 1080p30"
|
||||
]
|
||||
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1
|
||||
)
|
||||
|
||||
for line in process.stdout:
|
||||
progress = get_progress_from_line(line)
|
||||
if progress > 0:
|
||||
celery_app.send_task(
|
||||
"worker.ripping.update_progress",
|
||||
kwargs={
|
||||
"progress": progress,
|
||||
"status": "ripping",
|
||||
"message": f"Rippe Titel {progress}%"
|
||||
}
|
||||
)
|
||||
|
||||
process.wait()
|
||||
|
||||
if process.returncode == 0:
|
||||
return {
|
||||
"status": "success",
|
||||
"output_path": output_path,
|
||||
"return_code": process.returncode
|
||||
}
|
||||
else:
|
||||
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"
|
||||
else:
|
||||
return "unknown"
|
||||
|
||||
except Exception:
|
||||
return "unknown"
|
||||
|
||||
|
||||
def check_abcde_installed() -> bool:
|
||||
"""Prüft, ob abcde installiert ist."""
|
||||
return shutil.which("abcde") is not None
|
||||
@@ -113,113 +34,188 @@ def check_cdparanoia_installed() -> bool:
|
||||
return shutil.which("cdparanoia") is not None
|
||||
|
||||
|
||||
@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."""
|
||||
def get_progress_from_line(line: str) -> int:
|
||||
"""Extrahiert Fortschritt in Prozent aus HandBrake-Ausgabe."""
|
||||
match = re.search(r'(\d+\.\d+)%', 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"
|
||||
}
|
||||
|
||||
output_dir = f"/output/dvd/{disc_id}"
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
output_path = f"{output_dir}/dvd_{disc_id}.mkv"
|
||||
|
||||
return run_handbrake(device_path, output_path)
|
||||
return {"status": "error", "error": "HandBrakeCLI ist nicht installiert"}
|
||||
|
||||
|
||||
@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."""
|
||||
if not check_handbrake_installed():
|
||||
return {
|
||||
"status": "error",
|
||||
"error": "HandBrakeCLI ist nicht installiert"
|
||||
}
|
||||
|
||||
output_dir = f"/output/bluray/{disc_id}"
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
output_path = f"{output_dir}/bluray_{disc_id}.mkv"
|
||||
|
||||
return run_handbrake(device_path, output_path)
|
||||
|
||||
|
||||
@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."""
|
||||
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)
|
||||
|
||||
try:
|
||||
cmd = [
|
||||
"abcde",
|
||||
"-d", device_path,
|
||||
"-o", "flac",
|
||||
"-b",
|
||||
"-t", "1",
|
||||
"-a", "default",
|
||||
"-x",
|
||||
"-o", output_dir
|
||||
]
|
||||
|
||||
process = subprocess.Popen(
|
||||
cmd,
|
||||
build_handbrake_cmd(device_path, output_path),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
bufsize=1
|
||||
)
|
||||
|
||||
|
||||
for line in process.stdout:
|
||||
self.send_task(
|
||||
"worker.ripping.update_progress",
|
||||
kwargs={
|
||||
"progress": 50,
|
||||
"status": "ripping",
|
||||
"message": f"Rippe CD: {line.strip()}"
|
||||
}
|
||||
)
|
||||
|
||||
progress = get_progress_from_line(line)
|
||||
if progress > 0 and progress_cb:
|
||||
progress_cb(progress)
|
||||
|
||||
process.wait()
|
||||
|
||||
|
||||
if process.returncode == 0:
|
||||
# MusikBrainz Lookup (via abcde's post-read)
|
||||
self.send_task(
|
||||
"worker.ripping.update_progress",
|
||||
kwargs={
|
||||
"progress": 90,
|
||||
"status": "ripping",
|
||||
"message": "MusikBrainz Lookup läuft..."
|
||||
}
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
else:
|
||||
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)
|
||||
"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
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import os
|
||||
from celery_app import celery_app
|
||||
from ripping import rip_dvd, rip_bluray, rip_cd, detect_disc_type
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""Tests für ripping.py: Fortschritts-Parsing, Kommando-Bau, Disc-Erkennung.
|
||||
|
||||
Deckt genau die Stellen ab, an denen im Review 22.07. erfundene Schnittstellen
|
||||
gefunden wurden (abcde-Flags, Celery-API) — damit so etwas nie wieder still liegt.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
import ripping
|
||||
from ripping import (
|
||||
build_abcde_cmd,
|
||||
build_handbrake_cmd,
|
||||
detect_disc_type,
|
||||
get_progress_from_line,
|
||||
write_abcde_config,
|
||||
)
|
||||
|
||||
|
||||
def test_progress_parsing():
|
||||
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
|
||||
|
||||
|
||||
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_config_enthaelt_zielverzeichnis():
|
||||
pfad = write_abcde_config("/output/cd/test123")
|
||||
try:
|
||||
with open(pfad, encoding="utf-8") as f:
|
||||
inhalt = f.read()
|
||||
assert "OUTPUTDIR='/output/cd/test123'" in inhalt
|
||||
assert "INTERACTIVE=n" in inhalt
|
||||
finally:
|
||||
os.unlink(pfad)
|
||||
|
||||
|
||||
def test_handbrake_cmd_vollstaendig():
|
||||
cmd = build_handbrake_cmd("/dev/sr0", "/output/dvd/x/film.mkv")
|
||||
assert cmd[0] == "HandBrakeCLI"
|
||||
assert cmd[cmd.index("--input") + 1] == "/dev/sr0"
|
||||
assert cmd[cmd.index("--output") + 1] == "/output/dvd/x/film.mkv"
|
||||
assert "--preset" in cmd
|
||||
|
||||
|
||||
class _FakeResult:
|
||||
def __init__(self, stdout):
|
||||
self.stdout = stdout
|
||||
|
||||
|
||||
def test_disc_typ_erkennung(monkeypatch):
|
||||
faelle = [
|
||||
("UDF filesystem data 'MEIN_FILM' DVD Video", "dvd"),
|
||||
("data, BDMV bluray structure", "bluray"),
|
||||
("Audio CD, cda tracks", "cd"),
|
||||
("irgendwas anderes", "unknown"),
|
||||
]
|
||||
for ausgabe, erwartet in faelle:
|
||||
monkeypatch.setattr(
|
||||
ripping.subprocess, "run", lambda *a, _out=ausgabe, **k: _FakeResult(_out)
|
||||
)
|
||||
assert detect_disc_type("/dev/sr0") == erwartet
|
||||
|
||||
|
||||
def test_disc_typ_erkennung_fehler_gibt_unknown(monkeypatch):
|
||||
def kaputt(*a, **k):
|
||||
raise OSError("kein Geraet")
|
||||
|
||||
monkeypatch.setattr(ripping.subprocess, "run", kaputt)
|
||||
assert detect_disc_type("/dev/sr0") == "unknown"
|
||||
Reference in New Issue
Block a user