Restefeger: Auth komplett raus, Serien-Flow + Episoden-Matching, Jellyfin-Refresh, Duplikat-Warnung, echtes Nur-Hauptfilm
Ampel / ampel (push) Successful in 55s
Ampel / ampel (push) Successful in 55s
AUTH ENTFERNT (Commander-Entscheid 24.07., KONZEPT §10): /token- und /api-keys-Endpoints, auth.py, test_auth.py, passlib/bcrypt/PyJWT/ python-multipart, JWT_SECRET_KEY-Pflicht. Heimnetz-only, das UI hatte nie einen Login — die Auth-Oberflaeche war Placebo und die passlib/bcrypt- Falle brach die Ampel. Rate-Limit pro IP bleibt. Schnellstart laeuft jetzt ganz ohne .env-Pflichtwerte. Serien-Flow (Etappe-12-Kern, ARM-Wunde #395): - Rip-Dialog: Serienname + Staffel -> Ablage <Serie>/Season NN (jellyfin.org/docs Naming-Schema); tvshow.nfo + poster.jpg im Serien-Ordner, bei Staffel 2 nicht ueberschrieben. - Episoden-Matching per Laufzeitabgleich: HandBrakeCLI --scan ('+ duration:', handbrake.fr/docs) je MKV gegen TMDB-Staffel-Laufzeiten (GET /metadata/tv/{id}/season/{n}; tv-season-details-API). Ordnungserhaltend; komplette Staffel auf einer Disc klappt auch bei uniformen Anime-Laufzeiten (Sequenz-Stufe). Umbenannt wird NUR bei eindeutiger Zuordnung — sonst ehrliches Log. Mit Tests. Weitere Punkte: - Jellyfin/Emby-Bibliotheks-Refresh nach jedem fertigen Rip (POST /Library/Refresh, X-Emby-Token lt. jellyfin.org/docs) — URL/Key + Test-Knopf in Einstellungen -> Ripping. - Duplikat-Warnung: Disc-Fingerabdruck (jetzt Teil des Prescan-Ergebnisses + der Job-Metadaten) gegen die Historie; Karte zeigt 'bereits gerippt', Vollautomatik ueberspringt Duplikate. - 'Nur Hauptfilm' ECHT: makemkvcon info -> TINFO-Attr-9-Laufzeiten (usage.txt) -> laengster Titel -> mkv dev:X <nr>. Vorher wirkungsloses Setting; pro Rip im Dialog uebersteuerbar. Mit Tests. - OMDb-Treffer eingedeutscht via TMDB /find (external_source=imdb_id, de-DE; find-by-id-API). - Dashboard: Speicherplatz-Anzeige (amber < 60 GB) + CSV-Export (GET /jobs/export, Semikolon+BOM fuer deutsches Excel). - Metadaten-Seite entfernt (Abnahme durch Commander-Auftrag) inkl. Placebo-Endpoints /metadata/lookup (scannte Dummy-Device) und /metadata/confirm (schrieb nie gelesenen Cache-Key). - Doppel-Jahr-Fix: 'X (2009) (2009)' in Log und Ordnernamen. - Remote-Worker-Blocker: redis (6379) + postgres (5432) waren NIE veroeffentlicht — kein Remote-Worker konnte sich je verbinden. Ports jetzt offen (Heimnetz-Kompromiss, kommentiert) + API_URL fuer Worker. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -40,7 +40,7 @@ def check_cdparanoia_installed() -> bool:
|
||||
return shutil.which("cdparanoia") is not None
|
||||
|
||||
|
||||
def build_makemkv_cmd(device_path: str, output_dir: str) -> list:
|
||||
def build_makemkv_cmd(device_path: str, output_dir: str, titel: str = "all") -> list:
|
||||
"""Baut das MakeMKV-Kommando (pure Funktion, testbar).
|
||||
|
||||
-r Robot-Mode: maschinenlesbare Ausgabe (PRGV/MSG-Zeilen)
|
||||
@@ -48,7 +48,7 @@ def build_makemkv_cmd(device_path: str, output_dir: str) -> list:
|
||||
Container zum Hängen (1.18.4) bzw. Segfault (1.17.7):
|
||||
/sys zeigt dort auch Geräte ohne /dev-Knoten (Befund 23.07.)
|
||||
--progress=-same Fortschritt in denselben Stream wie die Meldungen
|
||||
mkv dev:<pfad> all <ziel> alle Titel der Disc verlustfrei als MKV
|
||||
mkv dev:<pfad> <titel> <ziel> 'all' oder eine Titel-Nummer (Hauptfilm)
|
||||
"""
|
||||
return [
|
||||
"makemkvcon",
|
||||
@@ -57,11 +57,74 @@ def build_makemkv_cmd(device_path: str, output_dir: str) -> list:
|
||||
"--progress=-same",
|
||||
"mkv",
|
||||
f"dev:{device_path}",
|
||||
"all",
|
||||
str(titel),
|
||||
output_dir,
|
||||
]
|
||||
|
||||
|
||||
def parse_tinfo_dauern(ausgabe: str) -> dict:
|
||||
"""TINFO-Zeilen → {titel_nr: dauer_sekunden} (pure Funktion, testbar).
|
||||
|
||||
makemkvcon-Robot-Format laut makemkv.com/developers/usage.txt:
|
||||
TINFO:<titel>,<attr_id>,<code>,"<wert>" — Attribut 9 ist die Laufzeit
|
||||
als "H:MM:SS" (ap_iaDuration).
|
||||
"""
|
||||
dauern = {}
|
||||
for zeile in ausgabe.splitlines():
|
||||
treffer = re.match(r'TINFO:(\d+),9,\d+,"(\d+):(\d{2}):(\d{2})"', zeile.strip())
|
||||
if treffer:
|
||||
nr, h, m, s = (int(g) for g in treffer.groups())
|
||||
dauern[nr] = h * 3600 + m * 60 + s
|
||||
return dauern
|
||||
|
||||
|
||||
def laengster_titel(dauern: dict):
|
||||
"""Titel-Nummer mit der längsten Laufzeit — der Hauptfilm (pure Funktion)."""
|
||||
if not dauern:
|
||||
return None
|
||||
return max(dauern, key=dauern.get)
|
||||
|
||||
|
||||
def lies_titel_dauern(device_path: str, timeout: int = 300) -> dict:
|
||||
"""Fragt die Titel-Laufzeiten der Disc ab (makemkvcon info, Robot-Mode)."""
|
||||
try:
|
||||
ergebnis = subprocess.run(
|
||||
["makemkvcon", "-r", "--noscan", "info", f"dev:{device_path}"],
|
||||
capture_output=True, text=True, timeout=timeout,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
return {}
|
||||
return parse_tinfo_dauern(ergebnis.stdout or "")
|
||||
|
||||
|
||||
def parse_scan_dauer(ausgabe: str) -> int:
|
||||
"""HandBrake-Scan-Ausgabe → Dauer in Sekunden (pure Funktion, testbar).
|
||||
|
||||
HandBrakeCLI --scan schreibt je Titel "+ duration: HH:MM:SS"
|
||||
(handbrake.fr/docs, Scan-Ausgabe); bei einer MKV-Datei gibt es genau
|
||||
einen Titel.
|
||||
"""
|
||||
treffer = re.search(r"\+ duration: (\d+):(\d{2}):(\d{2})", ausgabe)
|
||||
if not treffer:
|
||||
return 0
|
||||
h, m, s = (int(g) for g in treffer.groups())
|
||||
return h * 3600 + m * 60 + s
|
||||
|
||||
|
||||
def lies_datei_dauer(pfad: str, timeout: int = 120) -> int:
|
||||
"""Laufzeit einer MKV-Datei in Sekunden (HandBrakeCLI --scan)."""
|
||||
if not check_handbrake_installed():
|
||||
return 0
|
||||
try:
|
||||
ergebnis = subprocess.run(
|
||||
["HandBrakeCLI", "--scan", "-i", pfad],
|
||||
capture_output=True, text=True, timeout=timeout,
|
||||
)
|
||||
except (OSError, subprocess.TimeoutExpired):
|
||||
return 0
|
||||
return parse_scan_dauer((ergebnis.stdout or "") + (ergebnis.stderr or ""))
|
||||
|
||||
|
||||
def get_progress_from_prgv(line: str) -> int:
|
||||
"""Extrahiert Gesamt-Fortschritt (0-100) aus einer PRGV-Zeile.
|
||||
|
||||
@@ -178,7 +241,7 @@ def write_abcde_config(output_dir: str) -> str:
|
||||
return tmp.name
|
||||
|
||||
|
||||
def run_makemkv(device_path: str, output_dir: str, progress_cb=None) -> dict:
|
||||
def run_makemkv(device_path: str, output_dir: str, progress_cb=None, titel: str = "all") -> dict:
|
||||
"""Rippt eine DVD/Blu-ray verlustfrei mit makemkvcon; meldet Fortschritt."""
|
||||
if not check_makemkv_installed():
|
||||
return {"status": "error", "error": "makemkvcon ist nicht installiert"}
|
||||
@@ -187,7 +250,7 @@ def run_makemkv(device_path: str, output_dir: str, progress_cb=None) -> dict:
|
||||
|
||||
try:
|
||||
process = subprocess.Popen(
|
||||
build_makemkv_cmd(device_path, output_dir),
|
||||
build_makemkv_cmd(device_path, output_dir, titel),
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT,
|
||||
text=True,
|
||||
@@ -252,16 +315,27 @@ def run_makemkv(device_path: str, output_dir: str, progress_cb=None) -> dict:
|
||||
return {"status": "error", "error": str(e)}
|
||||
|
||||
|
||||
def rip_video(device_path: str, disc_id: str, disc_type: str = "dvd", progress_cb=None, output_dir: str = None) -> dict:
|
||||
def rip_video(device_path: str, disc_id: str, disc_type: str = "dvd", progress_cb=None,
|
||||
output_dir: str = None, nur_hauptfilm: bool = False) -> dict:
|
||||
"""Rippt eine DVD oder Blu-ray verlustfrei mit MakeMKV.
|
||||
|
||||
Bewusst KEIN eigener Celery-Task: der einzige Task ist worker.tasks.rip_disc,
|
||||
der hier mit seinem eigenen Fortschritts-Callback durchgreift. output_dir
|
||||
überschreibt das Ziel (Transcode-Fall: Roh-Rip nach /app/temp).
|
||||
nur_hauptfilm: Vorab-Info-Lauf, dann nur den LÄNGSTEN Titel rippen —
|
||||
das löst das eingestellte (vorher wirkungslose) mainFeatureOnly ein.
|
||||
"""
|
||||
if output_dir is None:
|
||||
output_dir = os.path.join(RIP_OUTPUT_DIR, disc_type, disc_id)
|
||||
return run_makemkv(device_path, output_dir, progress_cb=progress_cb)
|
||||
|
||||
titel = "all"
|
||||
if nur_hauptfilm:
|
||||
dauern = lies_titel_dauern(device_path)
|
||||
haupt = laengster_titel(dauern)
|
||||
if haupt is not None:
|
||||
titel = str(haupt)
|
||||
# Kein Titel ermittelbar → ehrlich auf 'all' zurückfallen statt raten
|
||||
return run_makemkv(device_path, output_dir, progress_cb=progress_cb, titel=titel)
|
||||
|
||||
|
||||
def rip_cd(device_path: str, disc_id: str, progress_cb=None, output_dir: str = None) -> dict:
|
||||
|
||||
Reference in New Issue
Block a user