# Real-Debrid-Downloader — Analyse & Verbesserungen (2026-05-23)

Tiefe Analyse via 3 parallele Subagents (Bugs / Features / UI) + 4 Design-Mockups.

---

## A. BUGS / ROBUSTHEIT (verifiziert gegen Quellcode)

Roter Faden: Die **Deferred-Post-Processing-Pipeline** (eingeführt um den Extract-Slot schnell freizugeben) ist nur halb ins Abbruch-/Lifecycle-Management integriert. Genau der Bereich des v1.7.156-Fixes.

### HOCH
- [ ] **H1 — Globaler Stop/Shutdown bricht Deferred-Post-Processing nicht ab.** `abortPostProcessing` (download-manager.ts:7053) iteriert nur über `packagePostProcessAbortControllers`, nie über `packageDeferredPostProcessAbortControllers`. Bei Stop/Shutdown/clearAll laufen MKV-Move/Archiv-Cleanup/Rename weiter, während synchron persistiert wird → FS-Zustand ≠ Session-State (halb verschobene Datei, halb gelöschtes Archiv).
- [ ] **H2 — Hybrid-Post-Extract feuert MKV-Collection/Rename als losgelöstes Promise** (download-manager.ts:11334). In keiner Tracking-Map, kein `shouldAbort`. Cancel/Reset während Hybrid-Collect → Dateien werden trotzdem verschoben/gelöscht.
- [ ] **H3 — 0-Byte-Datei wird als vollständig akzeptiert** wenn keine Größeninfo (download-completion.ts:129, source "stream-end"). Hoster antwortet HTTP 200 ohne Content-Length + schließt sofort → Item "Fertig" mit leerer Datei, kein Auto-Redownload.

### MITTEL
- [ ] **M1 — Deferred-Post-Extraction nicht in `packagePostProcessTasks`** (download-manager.ts:11974). Scheduler-Abschluss (8154) + finishRun (12310) sehen Deferred-Tasks nicht → Run-Ende/Summary feuert während noch Dateien verschoben werden, State-Reset mitten in FS-Arbeit.
- [ ] **M2 — `blockAllPersistence` wird nach Backup-Import nie zurückgesetzt** (app-controller.ts:678). Weiterarbeiten ohne Neustart → `persistSoon` ist dauerhaft No-Op → bei hartem Crash alle Änderungen weg.
- [ ] **M3 — `cancelPendingAsyncSaves` wartet nicht auf laufenden Async-Save** (storage.ts:1064). I/O-Overlap beim Import (Datenintegrität durch Generation-Guard geschützt, nur Robustheit).

### NIEDRIG
- [ ] **N1 — Toter Code in `findReadyArchiveSets`** (download-manager.ts:10847). Unbedingtes `ready.add+continue` macht strengeren Disk-Fallback-Block (untracked-pending-Schutz) unerreichbar.

**Empfehlung:** H1 + H2 + M1 zusammen fixen (eine kohärente Härtung der Deferred-Pipeline). H3 ist klein & unabhängig. M2 trivial.

---

## B. FEATURES / UX-GAPS (nach Mehrwert/Aufwand)

App läuft headless auf Windows-Server → Nutzer sitzt nicht davor. Größte Lücke: keine Benachrichtigungen.

1. [ ] **Webhook/Push-Benachrichtigungen** (Discord/Telegram/ntfy) — S–M. Bei Paket fertig/Fehler/Quota/Provider-down aufs Handy. Neuer `notifier.ts`, Hooks an Completion-Punkten. **Höchster ROI.**
2. [ ] **Fernsteuerung über bestehenden Debug-Server** (POST-Endpunkte) — S–M. Server hat schon HTTP + Token-Auth, aber nur GET. POST `/control/add-links`, `/start`, `/stop` → vom Handy steuern.
3. [ ] **URL-Duplikat-Erkennung beim Hinzufügen** — S. History-`urls` existiert, wird aber nie geprüft → versehentliche Re-Downloads verschwenden Quota. Warnen: "3 Links bereits geladen".
4. [ ] **Vereinheitlichter Pre-Flight-Check + Bulk-Skip toter Links** — M. Vor Start Größe/Name/Online für ganze Queue, "alle offline überspringen"-Button.
5. [ ] **Speicherplatz-Vorabprüfung vor Start** — S. Aktuell keine Free-Space-Prüfung → Abbruch mitten im Download bei voller Platte.
6. [ ] **Konsolidierte Fehler-Ansicht** — M. Alle fehlgeschlagenen Items flach + Fehlertext + "alle erneut versuchen".
7. [ ] **Per-Provider-Statistik** — M. Rohdaten (`providerTotalUsageBytes`) existieren, werden nur nicht dargestellt. Welches Abo lohnt sich?
8. [ ] **Auto-Retry fehlgeschlagener Pakete nach Wartezeit** — S–M. Quota/Cooldown-Fails am nächsten Tag automatisch neu versuchen.
9. [ ] **Plex/Jellyfin Library-Refresh nach MKV-Move** — S. Neue Folgen sofort sichtbar. Gleicher Hook wie #1.
10. [ ] **Watch-Folder für DLC/Link-Auto-Import** — M. Ordner überwachen → automatisch importieren+starten.

---

## C. DESIGN-MOCKUPS

4 Varianten in `design-mockups/` (index.html = Vergleich):
1. **Aurora** — verfeinerte Dark-Evolution (premium, vertraut, geringstes Risiko)
2. **Command** — Terminal/Ops-Dashboard (max. Dichte, Monospace, Status-LEDs)
3. **Vellum** — Light Editorial (warmes Papier, Serif, mutige helle Alternative)
4. **Nebula** — Neon/Synthwave (Magenta-Cyan-Glow, auffällig)

→ Nutzer wählt Richtung (oder Mischung).

---

## REVIEW / ERGEBNISSE (2026-05-23)

**Umgesetzt (v1.7.158):**
- ✅ **H1** — `abortPostProcessing` aborted jetzt auch alle Deferred- + Hybrid-Controller (globaler Stop/Shutdown/clearAll/external). Keine FS-Race gegen Shutdown-Save mehr.
- ✅ **H2** — Hybrid-Post-Extract läuft über neue `packageHybridPostProcessControllers`-Map (Set pro Package), Controller SYNCHRON vor dem detached Promise registriert, `shouldAbort` an Rename + MKV-Collect durchgereicht. `abortPackagePostProcessing` + `clearAll` räumen die Map. Cancel/Reset stoppt jetzt laufende Hybrid-Arbeit.
- ✅ **M1** — neuer `hasAnyDeferredPostProcessPending()`; Scheduler-Abschluss + `finishRun`-Clear gaten darauf. `hasDeferredPostProcessPending` (per-Package, für package_done-Cleanup) prüft jetzt auch Hybrid. Run endet erst wenn Background-FS-Arbeit fertig.
- ✅ **H3** — `validateDownloadedFileCompletion`: 0-Byte bei `stream-end` → `ok:false` (download_underflow), routet in den bestehenden Retry-Pfad. Regressionstest in `tests/download-completion.test.ts` (8 Tests).
- ✅ **N1** — toter Disk-Fallback-Block in `findReadyArchiveSets` + verwaiste `pendingItemStatus`-Map entfernt (verhaltensneutral).

**Bewusst NICHT umgesetzt (mit Begründung):**
- ✅ **M2** (`blockAllPersistence` nie zurückgesetzt) — GELÖST in v1.7.159 via **Auto-Relaunch**. In-Memory-Reload wäre unsicher (Task-finally-Blöcke settlen async gegen `this.session.items[id]` → Race beim Session-Swap, bräuchte async-Refactor). Stattdessen: nach erfolgreichem Import startet die App automatisch neu (main-getrieben in main.ts, nicht Renderer — robust gegen Renderer-Fehler). Der frische Prozess lädt die restored Session sauber via Standard-Startup-Pfad. `skipShutdownPersist`/`blockAllPersistence` schützen das ~1.5s-Fenster + den Quit (verifiziert: prepareForShutdown:5680 überspringt Persistenz sauber). Footgun eliminiert — User kann nicht mehr im blockierten Zustand weiterarbeiten.
- ⏭️ **M3** (`cancelPendingAsyncSaves` wartet nicht auf laufenden Save) — Report stuft selbst als reines I/O-Overlap ein; die Generation-Guard (storage.ts:1022) schützt die Datenintegrität bereits (stale Write wird verworfen). Kein Korrektheitsgewinn, daher kein Eingriff.

**Verifikation:** 30 Test-Dateien, 621 Tests grün. Build sauber. Advisor-Review vor Implementierung (fing H2-Falle: Hybrid-Controller nicht in die Deferred-Map legen, sonst killt `runDeferredPostExtraction` sie selbst).

---

## D. DEFERRED-PFAD RENAME-GAP (2026-05-28, Opus-Verifikation von 18eada9)

**Kontext:** Eine abgestürzte Session (API 400 thinking-blocks) hinterließ ein uncommittetes Working-Tree, das **drei** releaste Commits revertierte (08372f9 Passwort + 18eada9 Hybrid-Rename + 98dc366 Support-Bundle, zurück auf v1.7.159). Kein dokumentierter Intent → als Crash-Debris bewertet, non-destruktiv **gestasht** (`git stash` — recoverable), HEAD/v1.7.162 wiederhergestellt.

**Verifizierter Fund (Folge-Bug zu 18eada9):**
- 18eada9 schloss den "frische Datei landet unbenannt"-Bug nur für den **Hybrid-Pfad** (`deferFreshFiles=true` + Mehrfach-Pässe).
- Der **finale Deferred-Pass** (`runDeferredPostExtraction`) macht Rename (12125) → Collect (12156, `deferFreshFiles=false`). Ist eine Datei beim Deferred-Rename noch frisch (< `fileStabilizeMinAgeMs`, prod=2000ms) — v.a. eine eben per **Nested-Extraction** (12045, unmittelbar davor) geschriebene Datei — überspringt der Frische-Gate sie, und der Collect moved sie mit **Original-Scene-Namen** in die Library. `collectMkvFilesToLibrary` benennt selbst nicht um (Move-Body: `buildUniqueFlattenTargetPath`, nur Flatten).
- Pre-existierender Gap (Frische-Skip-Block älter als 18eada9); auch HEAD/v1.7.162 betroffen.

**Gate (TDD, vor Fix):** neuer Regressionstest "deferred final pass renames fresh files before collecting them" → reproduzierte den Bug zuverlässig gegen HEAD (Datei landete unbenannt).

**Fix (minimal, Root-Cause):** `treatFilesAsStable`-Param durch `autoRenameExtractedVideoFiles(Impl)`. Im Deferred-**Final**-Pass (kein concurrent Extractor-Write mehr, Extraktion awaited) wird der Frische-Gate umgangen → alle Dateien werden umbenannt, bevor der Collect sie sammelt. Hybrid-Pfad unangetastet (nutzt `...Impl` mit Default `false` → Frische-Skip bleibt aktiv, schützt weiter vor Rename mitten in concurrent Write).

**Verifikation:** neuer Test grün, Hybrid-Test grün (kein Regress), **623 Tests grün** (31 Dateien), tsc unverändert (9 pre-existing). Advisor-Gate vor Fix (verlangte Repro-Test statt Timing-Argument).

**Offen / bewusst nicht angefasst:**
- Gestashtes Crash-Debris (`stash@{0}`): enthält Revert von 08372f9/18eada9/98dc366 + log.old. Bei Bedarf inspizierbar/recoverbar; sonst irgendwann verwerfbar.
- 08372f9 (Passwort-Daemon-Reset) bewusst nicht neu aufgerollt (außerhalb dieses Goals, kein Hinweis auf Defekt).
- Untracked `*-postprocess/` + `fix-library-renames.mjs`: alte Experimente (Apr/Mai), unverändert gelassen.

---

## E. Mega-Debrid Account temporär deaktivieren (UI) — 2026-05-31

**Goal:** Einzelne Mega-Debrid-Accounts deaktivieren (statt löschen) → Rotation überspringt sie, nutzt die anderen.

**Befund:** Backend KOMPLETT vorhanden — `megaDebridDisabledAccountIds` (Typ/Defaults/Storage-Normalisierung) + Rotation-Skip (debrid.ts:1944) + Verfügbarkeits-Checks. Es fehlt NUR das UI-Toggle (Debrid-Link hat es bereits via keyStatsPopup). ID-Seam verifiziert: `getMegaDebridAccountId(login)` (trim+lowercase) ist auf beiden Seiten identisch → Test grün.

**Ansatz (B-kohärent):** Toggle in die bestehende Mega-Account-Liste im Bearbeiten-Dialog falten (draft-then-Save, KEIN Live-Persist → kohärent, minimal). Schritte:
1. [x] TDD: Test „skips a manually disabled Mega-Debrid account" (acc1 disabled → acc2) — grün gegen Backend.
2. [ ] `AccountDialogState`: Feld `megaDisabledIds: string[]`.
3. [ ] Dialog-Init (createAccountDialogState): aus `settings.megaDebridDisabledAccountIds`.
4. [ ] JSX Mega-Account-Liste: Aktivieren/Deaktivieren-Button + disabled-Styling pro Account.
5. [ ] Entfernen-Handler: ID auch aus megaDisabledIds entfernen.
6. [ ] `buildSettingsFromDialog` (megadebrid-api/web): `megaDebridDisabledAccountIds` aus Draft (gefiltert auf vorhandene Accounts) übernehmen.
7. [ ] Verifizieren: tsc unverändert, volle Suite grün, Toggle-Test grün.
