# Plan: „Nur deutsche Tonspur behalten" (.DL.) als Tool-Funktion Quelle der Idee: User-Script `Remove Non German Audio.py` (ffmpeg `-map 0:v:0 -map 0:a:0 -c copy -map_metadata -1`, + `.DL.`→`.` Rename). Soll als **toggle­barer Post-Extract-Schritt** nach jedem Entpacken laufen, nur für **MKV/MP4 mit `.DL.` im Namen** (Dual-Language), und nur die **deutsche** Spur behalten. Fundiert per 6-Agent-Analyse + Advisor. ## 1. Verhalten (Soll) - Läuft automatisch nach dem Entpacken eines Pakets (wenn Toggle an), bevor MKV-Collect. - Pro extrahierter Video-Datei mit `.DL.` im Namen (case-insensitive, nur .mkv/.mp4): 1. Audiospuren prüfen → deutsche/erste Spur bestimmen (Modus = User-Entscheidung, s.u.). 2. Wenn >1 Audiospur: remux (stream-copy, kein Re-Encode) → behält Video + 1 Audio (+ optional dt. Untertitel) → Temp-Datei → atomar ersetzen. 3. `.DL.` aus dem Dateinamen strippen (`.DL.`→`.`, `.DL`→``), Companion-Dateien (Untertitel/.nfo) mitziehen. 4. Wenn nur 1 Audiospur: **kein** Remux (spart Neuschreiben großer Dateien), ABER `.DL.`-Strip trotzdem. - Status pro Item sichtbar (z.B. „Tonspur wird bereinigt" / „Deutsche Spur behalten"). ## 2. Architektur - **NEUES Modul `src/main/video-processor.ts`** (spiegelt `extractor.ts`: exportierte async-Funktion + Options-Bag, KEINE DI-Klasse — es gibt keinen Constructor-Seam). Enthält: - ffmpeg/ffprobe-Spawn nach dem `runExtractCommand`-Muster (extractor.ts:1296): `spawn(cmd,args,{windowsHide:true})`, Promise-Wrapper, Timeout-Watchdog → `killProcessTree` (taskkill /T /F), **AbortSignal IN den Child** geben. - **Pure exportierte Helfer** für Unit-Tests: `pickGermanAudioTrack(probeJson, mode)`, `stripDualLangMarker(name)`, `buildFfmpegRemuxArgs(...)`, `computeRemuxTimeoutMs(bytes)`. - ffmpeg-Exit-Codes ≠ 7-Zip (NICHT die „exit 1 = ok"-Logik kopieren — nur das Spawn/Await/Kill-Gerüst). - ffprobe-JSON auf stdout NICHT durch den 48KB-Tail-Cap (`appendLimited`) — stdout separat voll puffern. - **ffmpeg-Discovery (Option a, empfohlen):** System-PATH + `RD_FFMPEG_BIN` env + lazy `ffmpeg -version`-Probe gecacht (spiegelt `RD_7Z_BIN`, extractor.ts:1030-1083). **Nicht bündeln** (~80-150MB → triggert den eigenen 150MB-Large-Bundle-Selfcheck debug-setup.ts:22 + GPL-Lizenzpflicht). Wenn ffmpeg fehlt → Schritt überspringen + WARN loggen + (optional) in Health-Check/Errors surfacen. NIE Downloads blockieren. - **CPU-Priorität:** `lowerExtractProcessPriority(pid, priority)` + `extractOsPriority` wiederverwenden, Priorität als **expliziten Param** (nicht das Modul-Global `currentExtractCpuPriority` — Cross-Talk-Gefahr). Honoriert `settings.extractCpuPriority`. ## 3. Einhängepunkte (BEIDE Pfade — kritisch!) Post-Processing ist **pro Paket**, zwei Pfade; Hybrid-Pakete durchlaufen NIE den Deferred-Pass: - **Deferred** (download-manager.ts ~11614): nach `autoRenameExtractedVideoFiles`, VOR archive-cleanup/collect. - **Hybrid** (download-manager.ts ~10944): zwischen Rename und Collect im detached Block. - Beide: **innerhalb `chainPackageFileOp(pkg.id, ...)`** (serialisiert Datei-Ops pro Paket), nur auf `pkg.extractDir` operieren — NIE im geteilten `mkvLibraryDir` (= der v1.7.107-revertierte Cross-Package-Crash; autoRename bricht bei Overlap ab, 3905-3919). - **Gate:** neuen Flag in den Post-Process-Aggregator OR-en (~7078-7084), sonst läuft der Schritt nie standalone. Hängt inhärent an `autoExtract` (braucht entpackte Dateien). - Datei-Enumeration: `collectVideoFiles(rootDir)` (rekursiv, SAMPLE_VIDEO_EXTENSIONS, constants.ts:28) — nur .mkv/.mp4 verarbeiten; Sample/Bonus-Dateien per vorhandenem Skip-Prädikat auslassen. ## 4. Der .DL.-Knoten (LÖST den „Feature no-op"-Fehler) - Selektion = „Datei hat `.DL.`"; der Schritt strippt `.DL.`. → KEIN früherer Schritt darf den Marker entfernen. - **autoRename NICHT ändern** (behält `.DL.` verbatim) → Marker überlebt bis zum Video-Schritt. - Video-Schritt läuft **nach** autoRename → sieht `.DL.` → remuxt + strippt `.DL.` atomar pro Datei. - **NUR `collectMkvFilesToLibrary.deriveCleanCollectFileName`** bekommt den `.DL.`-Strip als Post-Transform (läuft NACH dem Video-Schritt → kann den Selektor nicht brechen, verhindert nur Re-Einführung aus dem Ordner-Token). Companion-Files via `renameCompanionFiles`/`moveCompanionFiles` mitziehen. ## 5. Sicherheitsmodell (Original NIE verlieren) - Remux → Temp-Datei → Größe > 0 (idealerweise ~plausibel) prüfen → erst dann atomar ersetzen/umbenennen (`renamePathWithExdevFallback` + `verifyRenameAsync`). ffmpeg-Fehler/Abbruch → Temp löschen, Original bleibt. - **Disk-Space-Pre-Check**: vor Remux freien Platz ≥ Dateigröße (+Marge) prüfen, sonst skip+log (Temp verdoppelt transient den Platz auf einer Platte, die grad entpackt hat / parallel lädt). - **AbortSignal in den ffmpeg-Child** (Deferred-/Hybrid-Controller) → Stop/Cancel/Reset killt laufenden Remux. - **mtime erhalten** (`fs.utimes` nach Remux) → sonst überspringt Hybrid-Collect (deferFreshFiles=true) die frisch angefasste Datei. - **Sicherheits-Invariante (BEIDE Modi):** Original nur ersetzen, wenn die behaltene Spur sicher die richtige ist. Bei Unsicherheit (keine Tags / kein Deutsch gefunden) → Datei UNANGETASTET lassen + loggen, statt versehentlich die einzige brauchbare Spur zu löschen. - Dispositions-Flag der behaltenen Spur auf „default" setzen. - Best-effort pro Datei: ein Fehler markiert NICHT das Paket als failed und blockiert nicht den Collect anderer Dateien. ## 6. ffmpeg/ffprobe-Aufrufe (Stream-Copy, schnell) - Probe (nur im Tag-Modus): `ffprobe -v error -select_streams a -show_entries stream=index:stream_tags=language,title -of json INPUT` - Remux erste Spur (Script-Parität): `ffmpeg -i INPUT -map 0:v:0 -map 0:a:0 [-map 0:s? je nach Untertitel-Option] -c copy -map_metadata -1 -disposition:a:0 default -y TEMP` - Remux deutsche Spur (Tag-Modus): `-map 0:v:0 -map 0:a: ...` (Index aus ffprobe). ## 7. Settings/UI-Wiring (5 Pflicht-Stellen, +1 optional) 1. `src/shared/types.ts` AppSettings: `keepGermanAudioOnly: boolean` (+ ggf. `germanAudioMode`, `keepGermanSubs`, `ffmpegPath`). 2. `src/main/constants.ts` defaultSettings: `keepGermanAudioOnly: false` etc. 3. `src/main/storage.ts` normalizeSettings: `Boolean(...)` (Pfad: `asText`, NICHT normalizeAbsoluteDir → leer = System-ffmpeg). 4. `src/renderer/App.tsx` Settings-Tab „entpacken" neben collectMkvToLibrary: Toggle + eingerückte Sub-Optionen (disabled wenn aus). 5. `src/renderer/App.tsx` **emptySnapshot()-Literal** (~840-859) — sonst tsc-Fehler (Feld non-optional). 6. (optional) `src/main/support-data.ts` ~95: Flag in Diagnose-Export spiegeln. ## 8. Tests + Verifikations-Gate - ffmpeg in Tests **gemockt** (kein echter ffmpeg-Lauf): neues Modul via `vi.mock` in download-manager.test.ts (assert: korrekt aufgerufen + Sequenz nach autoRename / vor collect, Deferred + Hybrid). KEIN blankes `vi.mock("node:child_process")` in download-manager.test.ts (bricht echte Extractor-ZIP-Tests). - Separate `video-processor.test.ts`: `node:child_process` mocken → ffmpeg/ffprobe-ARGS asserten (Track-Wahl, Untertitel-Option). - Pure Helfer fs-frei testen (wie tests/auto-rename.test.ts): `pickGermanAudioTrack`, `stripDualLangMarker`. - Negativ-Test: Toggle aus → keine Verarbeitung. Edge: 1-Audio-`.DL.` → nur Rename, kein Remux. Kein-Deutsch → unangetastet. - **Gate:** tsc-Baseline = 6 vorbestehende Fehler (NICHT clean) → „keine NEUEN tsc-Fehler" + vitest 728→728+N grün + `npm run self-check` grün. ## 9. OFFENE ENTSCHEIDUNGEN (vor Bau — per AskUserQuestion) - **A. Spurauswahl:** Script-Parität (immer erste Audiospur, kein ffprobe, validiertes Verhalten) vs. Smart (deutsche Spur per Sprach-Tag, Fallback erste Spur, skip wenn kein Deutsch). - **B. Untertitel:** weglassen (wie Script) vs. deutsche Untertitel behalten. - **C. ffmpeg-Quelle:** nur System-PATH + `RD_FFMPEG_BIN` env vs. zusätzlich Settings-Pfad-Feld im UI. ## 10. Umsetzungsreihenfolge (nach Entscheidungen) 1. `video-processor.ts` + pure Helfer + deren Unit-Tests (TDD). 2. ffmpeg/ffprobe-Discovery (probe+cache). 3. Settings-Wiring (5 Stellen) + UI-Toggle. 4. Einhängen in Deferred + Hybrid (in chainPackageFileOp), Gate OR-en. 5. collect deriveCleanCollectFileName: `.DL.`-Strip-Safety-Net. 6. Logging (logRenameProcess, neuer Stage 'audio-strip'). 7. Tests (download-manager mock + video-processor args + negativ/edge). Gate prüfen.