"""Wo liegen die Roh-MKVs eines Jobs? — Suchen statt annehmen. ## Der Fund, der dieses Modul nötig gemacht hat (26.07.2026, live gemessen) Job `95afdc89` stand auf `failed`, und im Ablageziel lagen **79,6 GB** intakter Rohschnitt (`/app/media/rippy/95afdc89-…/title_t00.mkv`). Der SAVEPOINT v3.16 schrieb dazu: „→ ‚Neu komprimieren' genügt, kein Neu-Rip". Die Gegenprobe an der laufenden Instanz sagt: **`can_retry` war `false`** — den Knopf gab es gar nicht. Ursache: `_kann_neu_komprimieren` suchte an genau zwei Orten — im Container-Standard `/app/temp/raw/` und unter dem AKTUELLEN Wert der Einstellung `workDir`. Der Rip war aber mit einer Wahl *für diesen einen Rip* auf die NAS gelegt worden (das gibt es seit v3.15 im Rip-Dialog), und die Einstellung selbst stand auf leer. Damit zeigte nichts mehr auf die Datei: workDir (Einstellung) = "" → geprüft wurde nur /app/temp/raw tatsächlicher Ort = /app/media/rippy/ Ergebnis → 75 GB unsichtbar, Neu-Rip scheinbar unvermeidlich Das ist derselbe Fehler, der dieses Projekt schon mehrfach gekostet hat: aus einem Zustandswert (der heutigen Einstellung) auf einen Mechanismus (wohin damals gerippt wurde) geschlossen, statt nachzusehen. ## Warum gesucht und nicht gespeichert wird Den Ort in die Job-Zeile zu schreiben wäre sauberer — aber die jobs-Tabelle bräuchte eine neue Spalte, und `create_all` legt nur fehlende TABELLEN an, keine fehlenden Spalten. Eine Migration für einen Suchraum von einer Handvoll Verzeichnissen ist das falsche Werkzeug, und Bestandsjobs (genau der Fall hier) hätten den Wert ohnehin nicht. Der Suchraum ist nämlich klein und geschlossen: `_arbeitsverzeichnis()` im Worker lässt ausschließlich den Container-Standard oder einen Pfad UNTER `/app/media` zu. Es genügt also, `/app/temp/raw/` und `/` anzusehen — die oberste Ebene von `/app/media`, ohne Rekursion. Verwechslungsgefahr gibt es dabei nicht: Roh-Verzeichnisse heißen exakt wie die Job-ID (vollständige UUID), fertige Ablagen heißen `Titel (Jahr) [kurz-id]`. """ import posixpath # Container-Standard für Roh-Rips (RAW_DIR im Worker). RAW_STANDARD = "/app/temp/raw" MEDIA_ROOT = "/app/media" def kandidaten(job_id: str, work_dir: str, media_unterordner) -> list: """Alle Orte, an denen die Roh-MKVs dieses Jobs liegen KÖNNTEN (pure). `media_unterordner` sind die Namen der obersten Ebene unter /app/media (Ablageziele inkl. eingehängter Freigaben) — die Liste kommt vom Aufrufer, damit diese Funktion ohne Dateisystem testbar bleibt. Reihenfolge: Container-Standard, dann die eingestellte Wahl, dann alle Ablageziele. Doppelte fliegen raus, die Reihenfolge bleibt stabil. posixpath, nicht os.path: Das sind Container-Pfade. os.path.join baut unter Windows Backslashes daraus, und dann greift keine Prüfung mehr — dieselbe Falle wie bei `_zielbasis()` (v3.14) und `_mountpoint()` (26.07.2026). """ if not job_id: return [] orte = [posixpath.join(RAW_STANDARD, job_id)] wahl = (work_dir or "").strip().rstrip("/") if wahl and (wahl == MEDIA_ROOT or wahl.startswith(MEDIA_ROOT + "/")): orte.append(posixpath.join(wahl, job_id)) for name in media_unterordner or []: if name: orte.append(posixpath.join(MEDIA_ROOT, name, job_id)) gesehen, eindeutig = set(), [] for ort in orte: if ort not in gesehen: gesehen.add(ort) eindeutig.append(ort) return eindeutig def suche(job_id: str, work_dir: str, listdir, isdir) -> list: """Die Orte, an denen wirklich etwas liegt. `listdir` und `isdir` werden übergeben statt importiert — so ist die Suche ohne Dateisystem prüfbar, und ein toter CIFS-Mount kann hier keinen Import-Zyklus verursachen. """ try: unterordner = sorted(listdir(MEDIA_ROOT)) except OSError: unterordner = [] gefunden = [] for ort in kandidaten(job_id, work_dir, unterordner): try: if isdir(ort): gefunden.append(ort) except OSError: # Toter Mount → als „nicht da" werten. Ein Fehlschlag hier darf die # Job-Liste nicht mitnehmen (Befund 24.07. bei /storage-targets). continue return gefunden def groesse(pfade: list, listdir, isfile, getsize) -> tuple: """(Bytes, Dateizahl) der Roh-Dateien — flach, nicht rekursiv. Flach genügt: MakeMKV legt die Titel als `title_tNN.mkv` direkt in das Job-Verzeichnis, Unterordner entstehen dort nicht. """ bytes_gesamt, dateien = 0, 0 for pfad in pfade or []: try: namen = listdir(pfad) except OSError: continue for name in namen: voll = posixpath.join(pfad, name) try: if isfile(voll): bytes_gesamt += getsize(voll) dateien += 1 except OSError: continue return bytes_gesamt, dateien