fix(jobs): Rohdaten werden gesucht statt geraten - "Neu komprimieren" ging nicht
Ampel / ampel (push) Successful in 28s

Bei der Live-Gegenprobe des neuen /rohdaten-Endpunkts aufgefallen, und der Fund
ist groesser als der Endpunkt: Job 95afdc89 hatte `can_retry = false`, obwohl
79,6 GB intakter Rohschnitt unter /app/media/rippy/<id> lagen. Der Knopf
"Neu komprimieren" existierte gar nicht.

Der SAVEPOINT v3.16 schrieb dazu: "Rohschnitt 79,6 GB intakt -> 'Neu
komprimieren' genuegt, kein Neu-Rip." Das war falsch, und zwar doppelt:

  _kann_neu_komprimieren  suchte in /app/temp/raw/<id> und unter dem AKTUELLEN
                          workDir -> Knopf erschien nicht
  retry_transcode         berechnete raw_dir aus demselben aktuellen workDir
                          -> haette am falschen Ort gesucht

Ursache in beiden Faellen: Der Rip war mit einer Wahl NUR FUER DIESEN RIP auf
die NAS gelegt worden (gibt es seit v3.15), die Einstellung selbst stand auf
leer. Damit zeigte nichts mehr auf die Datei. Wieder derselbe Fehler, den dieses
Projekt schon mehrfach bezahlt hat: aus einem Zustandswert (der heutigen
Einstellung) auf einen Mechanismus (wohin damals gerippt wurde) geschlossen,
statt nachzusehen.

Neues Modul rohdaten.py sucht jetzt an allen Orten, die ueberhaupt in Frage
kommen: Container-Standard, eingestelltes Arbeitsverzeichnis und jedes
Ablageziel unter /app/media. Der Suchraum ist geschlossen, weil
_arbeitsverzeichnis() im Worker nur diese zulaesst; verwechseln kann man nichts,
weil Roh-Verzeichnisse exakt wie die Job-ID heissen (vollstaendige UUID) und
fertige Ablagen "Titel (Jahr) [kurz-id]".

Nicht in die Job-Zeile geschrieben, obwohl das sauberer waere: create_all legt
nur fehlende TABELLEN an, keine Spalten - und Bestandsjobs (genau dieser Fall)
haetten den Wert ohnehin nicht.

11 Tests, darunter der echte Fall, ein toter CIFS-Mount und der Klassiker
"/app/media-boese".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-07-26 13:09:44 +02:00
parent 843c54cd1a
commit 108368d583
3 changed files with 274 additions and 32 deletions
+41 -32
View File
@@ -18,6 +18,7 @@ import makemkv_key
import mounts as mount_verwaltung
import notify
import presets as preset_auswahl
import rohdaten
from celery_client import celery_client, start_rip
from detection import CDS_DISC_OK, CDS_NO_DISC, CDS_TRAY_OPEN, drive_status
@@ -308,14 +309,26 @@ async def root():
}
def _rohdaten_suchen(job_id: str, work_dir: str) -> list:
"""Wo liegen die Roh-MKVs dieses Jobs? (Details in rohdaten.py)"""
return rohdaten.suche(job_id, work_dir, os.listdir, os.path.isdir)
def _kann_neu_komprimieren(job: dict, work_dir: str) -> bool:
"""Nur wenn Rohdaten wirklich noch daliegen — der „Neu komprimieren"-Knopf
an einem Job, der nie gerippt hat, war Unsinn (Befund 24.07.)."""
an einem Job, der nie gerippt hat, war Unsinn (Befund 24.07.).
⚠️ Reparatur 26.07.2026, an der laufenden Instanz gemessen: Gesucht wurde
nur im Container-Standard und unter dem AKTUELLEN `workDir`. Der Rip von Job
95afdc89 lag aber auf der NAS, weil beim Start eine Wahl NUR FÜR DIESEN RIP
getroffen worden war (gibt es seit v3.15) — und die Einstellung selbst stand
auf leer. Ergebnis: `can_retry` war `false`, obwohl 79,6 GB intakt dalagen.
Der SAVEPOINT v3.16 behauptete „‚Neu komprimieren' genügt" — den Knopf gab
es nicht. Jetzt wird an allen möglichen Orten nachgesehen.
"""
if job.get("status") != "failed":
return False
if os.path.isdir(os.path.join("/app/temp/raw", job["id"])):
return True
return unter_wurzel(work_dir, MEDIA_ROOT) and os.path.isdir(os.path.join(work_dir, job["id"]))
return bool(_rohdaten_suchen(job["id"], work_dir))
@app.get("/jobs", response_model=List[Job])
@@ -347,27 +360,9 @@ async def get_jobs():
return await asyncio.to_thread(sammle)
def _rohdaten_verzeichnisse(job_id: str, work_dir: str) -> list:
"""Wo könnten Roh-MKVs dieses Jobs liegen? (beide möglichen Orte)"""
kandidaten = [os.path.join("/app/temp/raw", job_id)]
if unter_wurzel(work_dir, MEDIA_ROOT):
kandidaten.append(os.path.join(work_dir, job_id))
return [p for p in kandidaten if os.path.isdir(p)]
def _rohdaten_groesse(pfade: list) -> tuple:
"""(Bytes, Dateizahl) der Roh-MKVs — nur die Dateien, nicht rekursiv tief."""
bytes_gesamt, dateien = 0, 0
for pfad in pfade:
try:
for name in os.listdir(pfad):
voll = os.path.join(pfad, name)
if os.path.isfile(voll):
bytes_gesamt += os.path.getsize(voll)
dateien += 1
except OSError:
continue
return bytes_gesamt, dateien
"""(Bytes, Dateizahl) der Roh-MKVs (Details in rohdaten.py)."""
return rohdaten.groesse(pfade, os.listdir, os.path.isfile, os.path.getsize)
@app.get("/jobs/{job_id}/rohdaten")
@@ -389,7 +384,7 @@ async def job_rohdaten(job_id: str):
def sammle():
work_dir = os.path.normpath((db.get_settings().get("workDir") or "").strip() or "/")
pfade = _rohdaten_verzeichnisse(job_id, work_dir)
pfade = _rohdaten_suchen(job_id, work_dir)
bytes_gesamt, dateien = _rohdaten_groesse(pfade)
return {
"pfade": pfade,
@@ -419,7 +414,7 @@ async def delete_job(job_id: str, rohdaten: bool = False):
if rohdaten:
def raeume():
work_dir = os.path.normpath((db.get_settings().get("workDir") or "").strip() or "/")
pfade = _rohdaten_verzeichnisse(job_id, work_dir)
pfade = _rohdaten_suchen(job_id, work_dir)
bytes_gesamt, _ = _rohdaten_groesse(pfade)
for pfad in pfade:
shutil.rmtree(pfad, ignore_errors=True)
@@ -730,8 +725,15 @@ async def scan_tracks_ergebnis(name: str):
async def retry_transcode(job_id: str):
"""Stößt die Kompression eines Jobs neu an — OHNE die Disc neu zu rippen.
Voraussetzung: die Rohdateien liegen noch in /app/temp/raw/<job_id>
(bei Kompressions-Fehlschlägen bleiben sie dort absichtlich erhalten).
Voraussetzung: die Rohdateien liegen noch irgendwo (bei
Kompressions-Fehlschlägen bleiben sie absichtlich erhalten).
⚠️ Reparatur 26.07.2026: Das Roh-Verzeichnis wurde hier aus dem AKTUELLEN
Wert von `workDir` errechnet. Wer beim Rip-Start eine andere Ablage gewählt
hatte (gibt es seit v3.15), bekam damit einen Pfad, an dem nichts liegt —
und der Worker brach mit „Verzeichnis erreichbar, enthält aber keine
MKV-Datei" ab. Beim Job 95afdc89 lagen 79,6 GB auf der NAS, gesucht wurde in
/app/temp/raw. Jetzt wird nachgesehen statt gerechnet (rohdaten.py).
"""
job = await asyncio.to_thread(db.get_job, job_id)
if not job:
@@ -739,12 +741,19 @@ async def retry_transcode(job_id: str):
if job["status"] in ("running", "pending"):
raise HTTPException(status_code=409, detail="Job rippt noch")
# Roh-Verzeichnis: respektiert das konfigurierbare Arbeitsverzeichnis
# (Einstellungen → Verarbeitung), sonst Container-Default /app/temp/raw.
einstellungen = await asyncio.to_thread(db.get_settings)
work_dir = os.path.normpath((einstellungen.get("workDir") or "").strip() or "/")
raw_basis = work_dir if unter_wurzel(work_dir, MEDIA_ROOT) else "/app/temp/raw"
raw_dir = f"{raw_basis}/{job_id}"
gefunden = await asyncio.to_thread(_rohdaten_suchen, job_id, work_dir)
if not gefunden:
raise HTTPException(
status_code=409,
detail=(
"Keine Rohdaten zu diesem Job gefunden — weder unter "
"/app/temp/raw noch in einem der Ablageziele. Ohne sie muss die "
"Disc neu gerippt werden. (Gelöscht? Freigabe nicht eingehängt?)"
),
)
raw_dir = gefunden[0]
# Zielordner: der Worker schreibt das geplante Ziel beim Rip-Start nach
# output_path (sprechender Name statt UUID) — alter Fallback bleibt.
basis = job.get("target_dir") or f"{MEDIA_ROOT}/{job.get('disc_type') or 'bluray'}"