95c1f9b105
- Theme Context ausgelagert (ThemeContext.tsx + useDarkMode.ts) - Config Validation mit TMDB-API-Key Pflicht (config_validation.py) - Cache Key Centralization (cache/keys.py) - CD-Ripping mit abcde implementiert (Worker) - Docker Compose mit Healthchecks & LOG_LEVEL - ROADMAP.md & SAVEPOINT.md aktualisiert
226 lines
6.3 KiB
Python
226 lines
6.3 KiB
Python
import os
|
|
import subprocess
|
|
import re
|
|
import shutil
|
|
from celery_app import celery_app
|
|
|
|
|
|
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
|
|
|
|
|
|
def check_cdparanoia_installed() -> bool:
|
|
"""Prüft, ob cdparanoia installiert ist."""
|
|
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."""
|
|
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)
|
|
|
|
|
|
@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,
|
|
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()}"
|
|
}
|
|
)
|
|
|
|
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_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)
|
|
}
|