Headless-Server: Paket-Ausgaenge waren bisher nur per RDP+Log sichtbar. Neues
Modul notify.ts schickt einen fire-and-forget POST (ntfy-kompatibel: Title/
Priority/Tags als Header, Nachricht als Body) an eine konfigurierbare URL —
mit der kostenlosen ntfy-App aufs Handy, ohne Account/Port/Firewall (outbound).
- Settings: notifyUrl + 3 Ereignis-Toggles (Default aus) in Allgemein.
- Hook 1: Post-Processing-Ende (Paket completed/failed nach Entpacken).
- Hook 2: refreshPackageStatus fuer den Alle-Items-fehlgeschlagen-Fall (Link
tot -> Paket erreicht das Post-Processing nie; ohne diesen Hook schwiege
ausgerechnet der haeufigste Fehlerfall).
- Hook 3: finishRun mit Run-Summary (X/Y erfolgreich, Dauer, Schnitt).
- Dedup-Set pro Paket+Run, Lifecycle gespiegelt an historyRecordedPackages
(Run-Start-Clear, Retry-Deletes, removePackageFromSession). Guard
session.running || runPackageIds.has(id): nachlaufendes Entpacken nach
Run-Ende benachrichtigt noch, Startup-Recovery nach App-Neustart nicht
(sonst Doppel-Push fuer laengst fertige Pakete).
- 5s-Timeout, Fehler nur als logger.warn — blockiert nie den Download-Pfad.
- 9 Unit-Tests fuer notify.ts.
Der Error-Ring aus v1.7.185 war bisher nur ueber die Debug-Server-URL mit
Token erreichbar — per RDP ist ein Menueklick drastisch schneller als curl.
Neuer Menuepunkt im Hilfe-Dropdown zeigt die letzten 200 WARN/ERROR-Eintraege
(Kopf: "X Fehler, Y Warnungen") im bestehenden Bestaetigungs-Dialog mit
aufklappbaren Details; der Bestaetigen-Knopf kopiert die komplette Liste in
die Zwischenablage — direkt verwertbar fuer Bug-Reports.
IPC-Kette nach dem GET_DEBUG_SETUP_CHECK-Muster (ipc.ts, main.ts mit direktem
error-ring-Import wie debug-server/support-bundle, preload, preload-api).
Read-only auf den In-Memory-Snapshot, kein neues CSS.
Die Antwort auf "warum hat Paket X noch .DL.?" steht bisher nur in den
Rename-/Item-Logs — "kein Deutsch-Tag" ist INFO-Level und taucht nirgends im
UI auf. Jetzt speichert keepGermanAudioOnlyImpl pro Paket eine Zusammenfassung
(remuxed/kept-single/ohne-DE-Tag/ffmpeg-fehlt/Fehler + bis zu 100 Datei-Details
mit Aktion, Grund und erkannten Sprachen) direkt am PackageEntry:
- Status-Spalte zeigt "Tonspur: 5 OK / 1 ohne DE-Tag / ffmpeg fehlt" (rot bei
Auffaelligkeiten, flacher Stil), Datei-Details als Tooltip.
- Auch der ffmpeg-nicht-gefunden-Fruehausstieg schreibt die Summary.
- pkg.updatedAt wird gesetzt + Feld im Paket-Delta-Hash, damit der Snapshot
die Aenderung pusht; normalizeLoadedSession whitelistet das Feld mit
Shape-Validierung, sonst waere es nach jedem App-Neustart weg.
- 2 neue Integrationstests (Summary-Zaehler + ffmpeg-fehlt-Pfad).
importBackup wendete die Settings fuer beide Pfade ueber setSettings an, das bei
nicht-"never"-CleanupPolicy applyRetroactiveCleanupPolicy ausloest. Beim reinen
Settings-Restore purgte das die LIVE-Queue (fertige Items), obwohl der Vertrag
"running queue stays untouched" lautet (Dateien blieben auf Platte). Zudem rollte
der Import die laufenden Usage-/Status-Zaehler auf den (aelteren) Backup-Stand
zurueck (anders als updateSettings).
- setSettings bekommt optionales { suppressRetroactiveCleanup }; der Settings-only
Import setzt es. Die importierte Policy gilt weiter fuer KUENFTIGE Completions
ueber den normalen Vorwaertspfad (immediate/package_done) — nur der retroaktive
Sweep wird hier unterdrueckt.
- overlayLiveUsageCounters aus updateSettings extrahiert und im Settings-only Import
wiederverwendet (inkl. Key-Filter der Debrid-Link-Per-Key-Usage auf existierende
Keys). Nicht ueber updateSettings geroutet (vermeidet dessen resetHistoryForRetention).
Der Resume-Prune validiert Eintraege gegen die Top-Level-Archiv-Kandidaten auf
der Platte. Nested-Archiv-Schluessel (nested:<name>) haben dort kein Gegenstueck,
also wurden sie bei JEDEM extractPackageArchives-Aufruf geloescht — verschachtelte
Archive wurden beim Resume erneut entpackt. nested:-Schluessel werden im Prune
jetzt uebersprungen (sie werden mit dem Rest geleert, wenn das Paket fertig ist).
J: runPackagePostProcessing loescht im finally die Map-Eintraege fuer das Paket.
Hatte ein Abort den Handle schon entfernt und ein neuer Lauf einen frischen
Task+Controller gesetzt, riss das spaete finall des alten Tasks diesen neuen
Eintrag mit raus -> nicht abbrechbarer Waisen-Task + doppeltes paralleles
Post-Processing. Jetzt nur loeschen wenn Map noch auf DIESEN Task/Controller zeigt.
Q: collectFilesByExtensions filtert jetzt ~rd-Praefix (unsere Remux-Temp/Orphan-
Sidecars) aus, damit eine bei einem Crash mitten im Remux liegengebliebene
Teil-Datei nie in die MKV-Library gesammelt wird.
(dropItemContribution: Kommentar ergaenzt, dass das Nicht-Abziehen der
Session-Totals Absicht ist — kumulative Session-Zaehler, per Test abgesichert.)
flushAsync nahm eine Kopie der pending-Zeilen und entfernte sie nach dem await
per Index-Zaehlung (slice(snapshot.length)). Feuerte waehrend des awaits ein
write() den 1MB-Buffer-Cap, der vorne Zeilen wegshiftet, war die Zaehlung
desynchron und verwarf neu hinzugekommene, noch nicht geschriebene Zeilen.
Jetzt: pending-Zeilen per Move uebernehmen (Buffer auf [] zuruecksetzen) statt
kopieren; await-Zeit-writes laufen in einen frischen Buffer. Bei Schreibfehler
werden die Zeilen wieder vorn eingereiht und der Cap erneut angewandt.
- isGermanStream: Titel-Fallback nur noch ganze Woerter (german/deutsch); die
2-3-Buchstaben-Codes ger/deu sind im freien Titel-Text mehrdeutig und konnten
die falsche Spur als "deutsch" picken (und damit die echte deutsche loeschen).
Der Sprach-Tag-Check (ger/deu/de) bleibt unveraendert.
- looksLikeGermanRelease: 'dubbed' entfernt — ein nacktes "Dubbed" kann ein
italienischer/franzoesischer Dub sein und darf den German-first-Fallback nicht
ausloesen. Explizite german/deutsch-Tokens reichen.
- 2 Negativtests (3-Letter-Titel-Code, nicht-deutscher Dub).
Der atomare Ersetzen-Schritt loeschte das Original bevor der Ersatz bestaetigt
war; schlug das anschliessende Rename fehl (z.B. AV/Indexer-Lock), raeumte der
aeussere catch zusaetzlich die Temp-Datei weg -> null Kopien auf der Platte.
- Atomares Replace-over (MoveFileEx REPLACE_EXISTING / rename(2)) statt
rm-dann-rename: filePath haelt zu jedem Zeitpunkt entweder das volle Original
oder den vollen Remux.
- renameWithRetry: transiente Locks (EBUSY/EACCES/EPERM/EEXIST) mit Backoff
(200/500/1000ms) statt sofort abzubrechen.
- Eindeutiger Temp-Name (~rd<pid><rand>) statt fixem ~rdtmp -> keine Kollision
zwischen parallelen Paketen/Retries.
- 3 neue Tests (Recovery bei Replace-Fehler, Retry-Pfad EBUSY/EXDEV).
- tag mode: when no German-tagged audio track is found but the release name
says German/Dubbed, fall back to the first track (the dub is mislabeled, e.g.
German tagged "eng") instead of skipping; non-German names still skip safely
- comprehensive logging: per-package ffmpeg/ffprobe availability, plus per-file
detected audio languages, decision + reason, remux/rename result and the exact
error text when a file can't be processed
- shorter same-dir temp name so a long scene path + temp suffix cannot exceed
Windows MAX_PATH and silently fail the remux
When a Mega-Web account's unrestrict aborts because the shared unrestrict
timeout fired while it was running, give that account a 2-min cooldown
(only if it actually ran >=8s, so a quick user-cancel does not cool it
down). The download-manager retry then skips the cooled-down account and
rotates to the next one, instead of hammering the same account every 60s.
- debrid.ts: handle the abort in the rotation catch before classifyAccountFailure
- rotation log event TIMEOUT_COOLDOWN (+ renderer label) replaces the misleading
red "fataler Fehler" for this case
- RD_MEGA_ABORT_MIN_RUN_MS env override for the run-length threshold
- 2 regression tests (cooldown set -> next call rotates; quick abort -> no cooldown)
- New video-processor.ts: ffmpeg/ffprobe remux that keeps only the German
audio track (by language tag, with safe fallbacks) and strips the ".DL."
marker from the filename
- Runs after extraction in both the deferred and hybrid post-process paths,
inside the per-package file-op chain; abortable, disk-space checked,
mtime-preserving, atomic temp->replace so the original is never lost
- System ffmpeg via PATH / RD_FFMPEG_BIN; toggle + track-mode select in settings
- Electron crash handlers (render-process-gone, child-process-gone,
unresponsive/responsive, process warnings) with a circuit-breaker
auto-reload for renderer crashes
- Renderer error capture (window.onerror, unhandledrejection, React
ErrorBoundary) forwarded to the main log via a one-way IPC channel
- Memory-pressure heartbeat measured against the V8 heap_size_limit
- Gated DEBUG log level (RD_DEBUG) and an in-memory ring of recent
WARN/ERROR lines, exposed via the /errors endpoint and support bundle
- Disk-error classification (ENOSPC etc.) on download failures and
integrity-check pass/fail logging
Strip every comment from the source (parsed with the TypeScript compiler so
strings, template literals, regex literals and JSX are never touched), and drop
internal/working artifacts that do not belong in the public repository
(design mockups, internal analysis docs, a stray backup file and an old log).
No functional change: build is green, the full test suite passes.
Bei Serien, deren per-Episode-Ordner nur einen Episode-only-Token + Titel tragen
("Show.E01.Titel...-GRP", KEIN S01), benannte der Collect eine vom Auto-Rename bereits
korrekt benannte Datei ("Show.S01E01...-GRP.mkv") neu — und haengte den Staffel/Folgen-
Token HINTER die Scene-Gruppe ("...-GRP.S01E01"). In der Library stand dann der Episoden-
titel + ein angehaengtes S01E01 statt sauber S01E01 (gemeldet fuer "Steven Spielbergs Taken").
decideAutoRenameBaseName behaelt im Guard-B-Zweig "Ziel-Ordner ohne SxxExx" jetzt die
QUELLE, wenn sie ein nicht obfuskierter Scene-Name ist (sie traegt dort den einzigen echten
SxxExx-Token) — statt den Token an den Ordnernamen anzuhaengen. Obfuskierte/rohe Quellen
werden weiter aus dem Ordner sauber benannt. Wirkt in Collect und Auto-Rename.
Adversarial (Workflow) abgesichert: der Diskriminator ist allein "Quelle obfuskiert?" —
die Praefix-Laenge ist KEIN Kriterium, sonst fielen kurze Serien (ER, V, 24, Yu) durch und
zeigten denselben Bug. Regressionstest mit ER.S01E01 gepinnt. 4 Unit- + 1 Integrationstest.
Alte deutsche Dokus/Serien-Ordner ohne Gruppen-Suffix (Ordner endet auf bare Codec
".XviD", kein "-GROUP") wurden vom Auto-Rename als "kein Zielname" verworfen — die
Folge landete dann ROH in der Library (z.B. "safari-fm-s04e08a.avi" statt
"Fluss-Monster.S04E08a.Am.Essequibo.Teil.1.German.DOKU.SATRiP.XviD.avi").
buildAutoRenameBaseName akzeptiert jetzt zusaetzlich einen vollstaendigen Episoden-
Ordner: echter SxxExx-Token IM Ordnernamen UND ein Codec-/Aufloesungs-Marker
(SCENE_RESOLUTION_MARKER_RE / SCENE_CODEC_MARKER_RE, inkl. xvid/divx). Der Part-
Buchstabe a/b bleibt erhalten (Ordnername dient unveraendert als Zielname), sodass
Teil 1 und Teil 2 nicht kollidieren. Konservativ: ein nackter "Show.S01E01"-Ordner
ohne Qualitaets-/Codec-Marker wird weiterhin nicht abgeleitet. Greift in Auto-Rename
und Collect. 5 Unit- + 1 Collect-Integrationstest; v1.7.180-Fallback nutzt jetzt
dieselben Module-Konstanten (DRY).
Eine Folge mit gueltigem SxxExx-Token ist eine echte Episode, niemals Bonus/Extras —
auch wenn ihr Titel oder der Episoden-Ordnername ein Bonus-Wort enthaelt
(Interview/Outtakes/Special/Featurette/Making-Of/...). Bisher stufte der Library-
Collect (und Auto-Rename) solche Folgen als Extras ein und verschob sie NIE in die
Bibliothek — extrahiert und korrekt benannt, aber stumm liegengelassen (Skip nur via
logger.info, im Paket-Log unsichtbar). Betraf u.a. Revenge S04E19 "Interview".
Neue isBonusContent()-Guard an beiden Call-Sites: erst SxxExx pruefen (extractEpisodeToken),
nur ohne Token greift der Bonus-Filter (isInsideBonusDir / BONUS_FILENAME_RE). Echte Extras
ohne Token bleiben gefiltert. 2 Integrationstests + 5 Unit-Tests.
User-Report (Desktop-Log): "Kreuzfahrt ins Glück" — 25 Folgen "bet_kig_01_hdt.mkv" (obfuskiert,
KEIN SxxExx-Token) landeten roh in der Library, obwohl der Episoden-Ordner
"Kreuzfahrt.ins.Glueck.01.Hochzeitsreise.nach.Burma.2007.German.720p.HDTV.x264-BET" bereits der
saubere Name ist (Episode als "01" statt S01E01).
Ursache (vorbestehend, nicht v1.7.178/179): buildAutoRenameBaseName gibt null zurueck, sobald die
QUELLE keinen SxxExx-Token hat — das "Folge 01"-Nummernformat wurde nie unterstuetzt.
Fix: Fallback in decideAutoRenameBaseName — fehlt der Quell-Episode-Token und kann normal kein
Name abgeleitet werden, aber ein folderCandidate ist ein VOLLSTAENDIGER Scene-Release-Ordner
(Scene-Gruppe UND Aufloesung ODER Codec, kein reiner Season-Ordner), wird dieser Ordnername
direkt verwendet (note "folder-as-is"). Greift NUR ohne Quell-Episode-Token -> Mega-Direct
(mit Quell-Token) bleibt no-target. Aufloesung ODER Codec (nicht nur Aufloesung) deckt
DVDRip/XviD ohne 720p ab (Advisor-Punkt). Bonus/Sample werden vorher gefiltert.
Verifiziert: tsc 6, 682 Tests gruen (+3: Kreuzfahrt real, DVDRip-nur-Codec, Mega-Direct-bleibt-
no-target), Build gruen. Advisor + reproduzierter Diagnose-Test.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
User-Report (aus Desktop-Rename-Log): castle.s08e02.german.dl.720p.web.h264-idtv_int.mkv im
sauberen Episoden-Ordner "Castle.S08E02.GERMAN.DL.720p.WEB.H264-idTV_iNT" (Paket "scn2-cstl7")
wurde zu "scn2-cstl7.S08E02.mkv" VERSCHLIMMBESSERT (guter Quellname -> obfuskierter Paketname).
Ursache (vorbestehend, nicht durch v1.7.178): hasSceneGroupSuffix erkannte die Scene-Gruppe
"-idTV_iNT" nicht (SCENE_GROUP_SUFFIX_RE + Fallback verbieten Unterstriche). Der saubere
Episoden-Ordner wurde dadurch als Nicht-Scene-Ordner verworfen, und die Namensherleitung fiel
auf den obfuskierten Paket-Ordner "scn2-cstl7" zurueck -> "scn2-cstl7.S08E02".
Fix: hasSceneGroupSuffix nutzt jetzt zusaetzlich extractFlexibleSceneGroupSuffix (existierte
bereits, war aber nicht verdrahtet), das Unterstrich-Gruppen korrekt erkennt (splittet auf "_",
validiert jeden Teil). Der saubere Ordner wird akzeptiert -> idealer Name
"Castle.S08E02.GERMAN.DL.720p.WEB.H264-idTV_iNT". Mein v1.7.178-Folder-Token-Guard schuetzt
generische Paketordner (Mega-Direct) weiterhin.
Verifiziert: tsc 6, 679 Tests gruen (+1 Charakterisierung fuer den idTV_iNT-Fall), Build gruen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
User-Report (aus dem Desktop-Rename-Log): 17 Dateien landeten ROH in der Library
("tvarchiv...s07e12-720.mkv", "4sf-...s04e01.mkv") — Auto-Rename hatte sie verpasst, der
MKV-Collect schob sie mit dem rohen Scene-Namen weg.
Root Cause 1: Auto-Rename und collectMkvFilesToLibrary sind entkoppelte Scans. Auto-Rename
benennt nur present-and-stable Dateien in extractDir um; eine verpasste Datei (verpasster
Zyklus ODER lag in "Downloader Unfertig" ausserhalb extractDir) wurde von collect roh
weggeschoben (collect behielt blind den Basename).
Root Cause 2: decideAutoRenameBaseName fabrizierte Namen fuer token-lose generische Ordner
("Mega-Direct-Pack" -> "Mega-Direct-Pack.S01E01") wegen eines hasSceneGroupSuffix-Falsch-
Positivs auf "-Pack" — derselbe latente Bug haette Auto-Rename getroffen.
Fix:
- Namens-Entscheidung in EINE pure Funktion extrahiert: decideAutoRenameBaseName (Single
Source of Truth fuer Auto-Rename UND Collect — koennen nicht mehr divergieren).
- Wurzel-Schutz darin: Rename nur, wenn ein folderCandidate einen echten Season-/Episode-
Token traegt (kein Fabrizieren aus token-losen Ordnern). Fixt beide Pfade.
- collectMkvFilesToLibrary leitet den sauberen Namen via dieser Funktion ab (gegated auf
autoRename4sf4sj — respektiert die Umbenenn-Einstellung), inkl. Companion-Untertitel und
Dedup gegen den sauberen Namen. mkvFiles traegt jetzt sourceRoot fuer die Ordner-Herleitung.
- Auto-Rename-Loop nutzt jetzt die gemeinsame Funktion (behebt nebenbei 2 latente
use-before-declaration/TDZ-Fehler an resolveRenameItem).
- Latenter Bug: Casing-Zaehler renamedCount -> renamed (war undeklariert -> ReferenceError,
vom catch verschluckt -> Casing-Korrekturen wurden still verworfen).
Verifiziert: tsc 6 (von 9 — 3 latente Fehler nebenbei behoben), 678 Tests + 9 neue (7
Charakterisierung der Entscheidung + 2 Collect-Integration: raw->clean + Companion/.srt folgt
+ Datei ausserhalb extractDir), Build gruen. Adversarialer Review-Workflow (4 Linsen) + Advisor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beim Update parkte installUpdate() aktive Downloads via stop() -> deren Abbruch-
Continuation markierte die Items "cancelled"/"Gestoppt". autoResumeOnStart nimmt
nach dem Neustart aber nur "queued"/"reconnect_wait" auf, also liefen die gerade
ladenden Downloads nach dem Update nicht weiter (timing-abhaengig: "manchmal").
Jetzt: stop({parkForRestart:true}) bricht aktive Tasks mit Grund "shutdown" ab,
sodass sie als "queued" re-queued werden (wie bei normalem App-Shutdown). Das
schliesst zugleich den einzigen plausiblen Loesch-Pfad (all-cancelled-Pakete sind
ueber applyRetroactiveCleanupPolicy entfernbar). Stop-Button-Verhalten unveraendert.
Zusaetzliche Robustheit in storage.ts (enge Blast-Radien, nicht die Hauptursache):
- async-Save-Clobber: eine gequeuete, veraltete Payload konnte einen neueren
Sync-Save (persistNowSync/prepareForShutdown) ueberschreiben; Generation wird
jetzt zum Snapshot-Zeitpunkt erfasst und durch die Queue getragen.
- loadSession gab leer zurueck (und ignorierte ein gefuelltes .bak), wenn die
Primaerdatei fehlte; faellt jetzt auf die Backup/Temp-Recovery zurueck.
Regressionstests: tests/update-restart-resume.test.ts (echter Live-Download ->
Park -> Reload = queued, plus Charakterisierung plain stop() -> cancelled) und
tests/session-restart-loss.test.ts (Clobber + Backup-Fallback). Volle Suite gruen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
User-Goal: bei kuenftigen Renaming-Problemen eine vollstaendige, sofort auffindbare Uebersicht —
JEDER Umbenenn-/Verschiebevorgang protokolliert UND danach verifiziert (liegt die Datei wirklich
unter dem Zielnamen? Quelle weg? richtige Schreibweise?).
- NEU desktop-rename-log.ts: pro Sitzung <Desktop>/Downloader-Log/rename-session_<ts>.txt; Ordner
selbstheilend (mkdir recursive vor jedem Write -> auch nach Loeschung zur Laufzeit sofort wieder
da). Synchroner Append, Schreibfehler verschluckt (bricht nie einen Download).
- verifyRename (sync) + verifyRenameAsync (Hot-Path): prueft Ziel-Existenz, echten On-Disk-Namen
(case-genau via readdir), Quell-Abwesenheit; Level INFO/WARN/ERROR. Nutzt denselben \?\-Long-
Path-Prefix wie der echte Rename (sonst falsche Urteile auf langen Scene-Pfaden).
- download-manager: renamePathWithExdevFallback = verifizierter Wrapper um die unveraenderte
Raw-Logik (deckt alle Media-Renames ab) + 3 Sync-Sites (startup-Dedup, Deobfuskation, Suffix-Fix)
via logVerifiedRenameSync; logRenameProcess spiegelt ins Desktop-Log.
- app-controller init/shutdown (getPath("desktop") gegen Startup-Crash abgesichert); support-bundle
packt das Log mit ein.
Adversarialer Review-Workflow (4 Linsen) fand + behoben: Long-Path-Verify-Bug (falsches OK
maskiert halb-fertigen Move), readdir-Fehler-False-OK, sync-I/O im Hot-Path, getPath-Guard,
Test-Temp-Cleanup. tsc 9 (Baseline), 663 Tests (+7 neue), Build gruen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
User-Entscheidung: ein Mega-Debrid-Account am Tageslimit soll bis zum Programm-Neustart
uebersprungen werden, nicht alle 20s/2min neu getestet.
Ground Truth (Support-Bundle gegrept): der limitierte Account liefert im Web-Pfad NIE eine
unterscheidbare Meldung — "Kein Server" = 0 Treffer, "Antwort leer" = 20.861. Tageslimit und
transienter Blip sind auf Message-Ebene nicht trennbar (generate() findet ohne processDebrid-
Code keinen Code -> return null -> "Antwort leer"). Ein Trigger auf "Kein Server" waere toter Code.
Loesung (Verhaltens-Signal statt Wortlaut):
- megaDebridEmptyResponseStreaks zaehlt aufeinanderfolgende "Antwort leer"/"Kein Server"-
Treffer je Account; ab 3 wird der Account bis Neustart geparkt (until=MAX_SAFE_INTEGER,
nur In-Memory -> Neustart loescht). Erfolg/anderer Fehler setzt zurueck.
- classifyAccountFailure markiert beide Signale als limitSignal (Symmetrie: ein einzelner
evtl. transienter Treffer parkt NICHT, behaelt kurzen Cooldown).
- Skip-Branch: "uebersprungen (bis Neustart gesperrt)", traegt nicht zu earliestCooldownUntil
bei (kein absurder Retry-Timer); Post-Loop wirft klare Endmeldung wenn alle geparkt.
- generate() surfacet "Kein Server" zusaetzlich als Page-Error (falls es doch im HTML steht).
- UI: Rotations-Verlauf zeigt "bis Neustart gesperrt".
Verifiziert: tsc 9 (Baseline), 655 Tests + 5 neue (inkl. Wiring-E2E der eine echte leere
Antwort durch unrestrictWithAccounts->classify->catch->Park treibt), Build gruen.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
User-Report: Logs zeigten z.B. "17:29:43" obwohl es lokal 19:29:43 war (CEST/UTC+2), weil
alle Logger `new Date().toISOString()` (UTC "...Z") nutzten. Neuer Helper logTimestamp()
formatiert lokale Zeit mit explizitem Offset (ISO 8601, z.B. "2026-05-31T19:29:43.605+02:00")
— menschlich lokal UND weiterhin eindeutig/Date.parse-bar. Angewandt auf alle Log-Zeilen-
Writer: item-log, logger (rd_downloader.log), audit-log, rename-log, session-log,
package-log, account-rotation-log, trace-log. Interne/API-/Dateinamen-Zeitstempel
(debug-server, support-bundle, trace autoDisableAt-Config) bleiben absichtlich UTC.
Test: tests/log-timestamp.test.ts (Format + Round-Trip zum selben Instant + lokale Stunde,
TZ-unabhaengig). 650 Tests gruen, tsc 9, Build sauber.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Root-Cause (verifiziert via Support-Bundle): Der Web-Unrestrict lief fuer JEDEN rotierten
Account mit den Creds des ersten/Legacy-Accounts (settings.megaLogin), weil MegaWebFallback
EINE geteilte Cookie-Session + festes getCredentials() nutzte UND megaWebUnrestrict ohne
Account-Bezug aufgerufen wurde. Item-Log-Beweis: "Account 2/2 (FabelDavid): Mega-Web Antwort
leer", obwohl FabelDavid real funktioniert — die Rotation nutzte FabelDavid nie wirklich,
sondern immer Account 1 (am Limit). Alle bisherigen Fixes (v1.7.169-172) lagen downstream
dieses Punkts und konnten den Bug nicht beheben.
Fix:
- MegaWebUnrestrictor bekommt optionalen `account`-Parameter; MegaDebridClient.unrestrictViaWeb
reicht this.login/this.password (den rotierten Account) durch; app-controller leitet ihn weiter.
- MegaWebFallback: Per-Login Session-Cache (Map<login,{cookie,setAt}>) statt einem geteilten
Cookie; login() gibt das Cookie zurueck, generate() bekommt es als Param. Jeder Account nutzt
seine eigene Session — kein Re-Login-Thrash unter Parallel-Last (maxParallel=8).
Tests: mega-web-fallback (Login-POST traegt den uebergebenen Account-Login, nicht den Default)
+ debrid-Rotation (jeder Account erhaelt SEINE Creds; Account 2 loest auf). 647 Tests gruen,
tsc 9, Build sauber.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
User-Report: Account 1 am Tageslimit liefert "Kein Server fuer diesen Hoster verfuegbar".
Bisher lief das durch die volle Web-Retry-Maschine (generate->null -> re-Login -> 3x
REQUEST_RETRIES) und fraß ~40s des GETEILTEN 60s-Unrestrict-Budgets -> der funktionierende
naechste Account (FabelDavid) lief in den Timeout (aborted:debrid -> als fatal klassifiziert,
"abgebrochen (fataler Fehler)" im Rotations-Verlauf), obwohl er gehen wuerde.
Fix (3 Teile, gemeinsame MEGA_DEBRID_NO_SERVER_RE):
1. mega-web-fallback generate(): die "Kein Server"-Meldung wird surfacet (throw) statt
null zurueckzugeben -> kein re-Login + erneutes Pollen.
2. unrestrictViaWeb: bricht bei der Meldung ab (kein 3x-REQUEST_RETRIES) -> sofortige
Retries sind zwecklos (Limit bleibt) und verbrennen das geteilte Rotations-Budget.
3. classifyAccountFailure: erkennt die Meldung -> quota-Cooldown (2 min) -> naechster
Account, mit echter Meldung im Log statt generischem "Antwort leer".
So scheitert der limitierte Account schnell (1 Versuch) und der naechste Account bekommt
das volle Budget zum Aufloesen.
Tests: mega-web-fallback (throw + ajaxCalls=1) + debrid-Rotation (acc1 Limit -> acc2,
calls=2). 645 Tests gruen, tsc 9, Build sauber.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Folge-Fix zum Sofort-Check (efb5696): updateSettings uebernahm debridAccountStatuses
aus dem (evtl. veralteten) Renderer-Settings-Patch statt aus dem Manager. Speichern
direkt nach Hinzufuegen+Pruefen eines Accounts konnte so den frisch geprueften Status
ueberschreiben -> Badge sprang zurueck auf "Noch nicht geprueft". debridAccountStatuses
ist main-owned Runtime-State (nur via applyDebridAccountStatuses gesetzt) -> wird in
updateSettings jetzt aus dem Live-Manager bewahrt (wie die Usage-Counter). Schuetzt auch
den "Alle pruefen"+Settings-Save-Pfad. 643 Tests gruen, tsc 9.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Beim "Hinzufuegen" eines Mega-Debrid-Accounts im Bearbeiten-Dialog wird der Account
jetzt sofort einzeln geprueft (connectUser) — Login-Gueltigkeit + Premium-Restlaufzeit
erscheinen direkt als Badge, ohne den Tab schliessen und "Alle pruefen" klicken zu
muessen. Waehrend der Pruefung zeigt das Badge "Pruefe…".
Neue Einzel-Check-IPC (Spiegel von checkDebridAccounts): CHECK_MEGA_DEBRID_ACCOUNT
-> app-controller.checkSingleMegaDebridAccount(login, password) baut den Account-Entry
(id via getMegaDebridAccountId), ruft die bestehende checkMegaDebridAccount() und merged
das Ergebnis via applyDebridAccountStatuses -> Snapshot -> Badge (gleicher Pfad wie
"Alle pruefen"). Funktioniert fuer den noch nicht gespeicherten Draft-Account, weil
applyDebridAccountStatuses merged (kein Pruning) + emitState.
Kern-Check-Logik unveraendert + weiterhin durch account-check.test.ts gedeckt.
643 Tests gruen, tsc 9 (unveraendert), Build sauber. GUI compile-/build-verifiziert,
im laufenden Electron noch nicht click-getestet.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Backend war bereits vorhanden (megaDebridDisabledAccountIds + Rotation-Skip +
Storage-Normalisierung); es fehlte nur das UI. Spiegelt das Debrid-Link-Muster:
im Account-Bearbeiten-Dialog bekommt jeder Mega-Account einen Aktivieren/
Deaktivieren-Toggle (+ "Deaktiviert"-Badge). Der Disabled-Zustand wird im Dialog-
Draft gehalten (megaDisabledIds) und beim Speichern via applyAccountDialogToSettings
in megaDebridDisabledAccountIds uebernommen (gefiltert auf vorhandene Accounts).
Kein Live-Persist mitten im Dialog -> kohaerent mit dem draft-then-Save-Modell.
Wirkt OHNE Neustart: DebridService.unrestrictLink liest this.settings live
(setSettings propagiert die Liste), unrestrictWithAccounts ueberspringt deaktivierte
Accounts (gleicher Mechanismus wie Daily-Limit/Cooldown-Skip).
Test: "skips a manually disabled Mega-Debrid account" — acc1 disabled -> acc2 loest
auf (beweist den ID-Seam getMegaDebridAccountId). 643 Tests gruen, tsc 9, Build sauber.
GUI-Toggle compile-/build-verifiziert, im laufenden Electron noch nicht click-getestet.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
v1.7.168 fuehrte einen 25s-Per-Account-Timeout in die Rotation ein, Annahme: ein
"haengender" Account solle uebersprungen werden. Falsch: der Mega-Debrid-WEB-Unrestrict
ist eine Polling-Schleife (mega-web-fallback.ts: bis 60 Durchlaeufe, intern 180s-Ceiling)
— Mega-Debrid laedt die Datei erst auf den eigenen Server, das dauert legitim 30-180s.
Der 25s-Cap schnitt JEDEN Account mitten im Polling ab ("Account-Timeout nach 25s" in
Dauerschleife), die Datei wurde nie aufgeloest. Ein Timeout ist bei einem langsam-
pollenden Provider KEIN Account-Fehler und darf keine Rotation ausloesen.
Revert auf den Stand vor c4a49d9: Rotation nur noch bei echten Account-Fehlern
(Quota/Ban/ungueltig -> Cooldown -> naechster). debrid.ts + debrid.test.ts (inkl. des
dedizierten Per-Account-Timeout-Tests) zurueckgesetzt. 641 Tests gruen, tsc 9 (unveraendert).
WICHTIG: Behebt nur die von mir verursachte Regression — macht den Download NICHT von
selbst funktionsfaehig. Offene Frage (Mega-Web langsam-aber-funktioniert vs. Server-IP
geblockt) ist erst zu klaeren, bevor am eigentlichen Unrestrict-Timeout gedreht wird.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Kernbug (User-Log, v1.7.168): "Unrestrict Timeout nach 60s" — Account 1 hing die
volle Zeit, acc2/acc3 wurden NIE versucht. Ursache: die gesamte Account-Rotation
lief unter EINEM geteilten ~60s-Signal (download-manager wickelt den ganzen
unrestrictLink in getUnrestrictTimeoutMs()); haengt acc1 bis es feuert, bricht die
ganze Rotation ab.
Fix (debrid.ts): jeder Account/Key bekommt im Rotations-Loop sein EIGENES Timeout
(PER_ACCOUNT_ATTEMPT_TIMEOUT_MS=25s, env RD_PER_ACCOUNT_TIMEOUT_MS, clamp 8-45s) via
AbortController + AbortSignal.any([global, attempt]). Catch: globaler signal.aborted
-> throw (Rotation stoppen); nur attemptController.signal.aborted -> 30s-Cooldown +
naechster Account. Ueber die Retry-Zyklen werden mit den Cooldowns alle Accounts erreicht.
Test: "aborts Mega web unrestrict when caller signal is cancelled" pruefte vorher
Objekt-Identitaet (.toBe(controller.signal)); der Per-Account-Timeout wrappt das Signal
aber zwingend (AbortSignal.any), die gereichte Instanz ist daher absichtlich nicht mehr
identisch. Umgestellt auf VERHALTEN: gereichtes Signal ist eine AbortSignal-Instanz und
propagiert das Caller-Cancel (aborted=true).
Recovered aus dem reset-weggesetzten Commit ae3ee1f (der andere Chat committete den Fix,
der Test brach, er resettete + hing). 641 Tests gruen, tsc unveraendert (9 pre-existing).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
User sah im Item-Log nur "Link-Umwandlung gestartet" -> "Unrestrict Timeout
60s" -> "erneut Versuch 1/inf", aber nie welcher Account/Key wann probiert
wurde. Die Rotation lief nur in account-rotation.log + Panel.
Jetzt: AsyncLocalStorage-Item-Sink (parallel-sicher bei 8 gleichzeitigen
Unrestricts) leitet JEDEN Rotations-Event in das Log des betroffenen Items:
"Account-Rotation: Mega-Debrid Web - Account 1 (xy) wird versucht / fehl-
geschlagen (Timeout) -> Account 2". Damit ist im Item-Log direkt sichtbar,
ob acc2/acc3 ueberhaupt erreicht werden -> dient auch als Diagnose fuer den
vermuteten Timeout-Bug (kommt separat, falls das Log Stillstand zeigt).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Folge-Fund zu 18eada9 (Opus-Verifikation des deferFreshFiles-Konzepts):
18eada9 schloss den "frische Datei landet mit Original-Scene-Namen in der
Library"-Bug nur fuer den Hybrid-Pfad (deferFreshFiles=true + Mehrfach-Paesse).
Der finale Deferred-Pass blieb betroffen.
Root Cause (verifiziert via failing Test gegen HEAD):
- runDeferredPostExtraction macht Rename -> Collect (deferFreshFiles=false). Ist
eine Datei beim Deferred-Rename noch "frisch" (juenger als fileStabilizeMinAgeMs,
prod=2000ms) -- v.a. eine eben per Nested-Extraction geschriebene Datei --
ueberspringt der Frische-Gate sie, und der Collect moved sie mit Original-
Scene-Namen in die Library. collectMkvFilesToLibrary benennt selbst nicht um
(buildUniqueFlattenTargetPath, nur Flatten).
- Im Deferred-FINAL-Pass gibt es keinen concurrent Extractor-Write mehr
(Extraktion inkl. Nested ist awaited) -- der Frische-Gate ist dort ein False
Positive. Pre-existierender Gap (Frische-Skip aelter als 18eada9), auch
v1.7.162 betroffen.
Fix (minimal): treatFilesAsStable-Param durch autoRenameExtractedVideoFiles(Impl).
Der Deferred-Final-Pass ruft mit treatFilesAsStable=true -> Frische-Gate umgangen
-> alle Dateien werden umbenannt, bevor der Collect sie sammelt. Hybrid-Pfad
unangetastet (nutzt ...Impl mit Default false -> Frische-Skip bleibt aktiv).
Regressionstest: frische Datei im Deferred-Pass landet UMBENANNT in der Library.
623 Tests gruen, tsc unveraendert (9 pre-existing).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Das Bundle packte bisher alle logs-Unterordner rekursiv (addDirectoryIfExists)
→ tausende Per-Item-Logs → 200+ MB (User-Bundle: 4273 Item-Logs, 214 MB).
Zum Verschicken/Analysieren unhandlich.
Neue addRecentDirectoryFiles(): packt nur Dateien mit mtime in den letzten
8h. Package-Logs und Item-Logs nutzen das 8h-Fenster; Session-Logs (wenige
Dateien) weiterhin komplett. Haupt-Logs (rd_downloader.log, rename.log,
audit.log, session.log, trace.log) waren schon per addFileIfExists einzeln
gepackt und bleiben unveraendert. Live-Logs der aktiven Queue (laufende
Session) ebenfalls komplett.
Ergebnis: Bundle enthaelt alles fuer aktuelle Fehler + Rename-Probleme,
aber kein Bloat durch tausende alte Item-Logs.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User-Report (verifiziert via Support-Bundle): pl3x-24hours.s01e07,
tmsf-burnnotice-s05e11-repack, -s05e15 landeten mit Original-Scene-Namen in der
Library statt umbenannt. Andere Episoden derselben Pakete (formatidentisch)
wurden korrekt umbenannt → kein Format-Problem, sondern Timing-Race.
Root Cause (aus Log-Timeline):
1. autoRenameExtractedVideoFilesImpl erfasste `now` EINMAL am Scan-Start. Bei
Hybrid-Extraktion werden weitere Dateien WÄHREND des Scans geschrieben →
deren mtime > now → negatives ageMs → der "Clock-Skew = stabil"-Zweig wertete
sie faelschlich als stabil → Rename mitten im Extractor-Write → EBUSY → 200ms-
Retry deferred.
2. Der MKV-Collect hatte KEINEN Frische-Skip und moved die Datei im Retry-Fenster
mit Original-Namen, bevor der Rename-Retry feuerte.
3. Rename + Collect liefen als zwei separate chainPackageFileOp-Ketten →
ueberlappende Hybrid-Runden konnten einen Collect zwischen Rename und Collect
einer anderen Runde einschieben.
Fix (3 Teile, scoped auf extractDir des Pakets — kein Shared-Library-Scan, nicht
das v1.7.107-Antipattern):
1. `now` wird PRO DATEI erfasst → frisch-geschriebene Dateien korrekt als "frisch"
erkannt und deferred (statt EBUSY-Rename mitten im Write).
2. collectMkvFilesToLibrary bekommt deferFreshFiles-Param: im Hybrid-Pfad werden
frische Dateien (juenger als fileStabilizeMinAgeMs) uebersprungen statt unbenannt
gemoved. Der finale Deferred-Pass (deferFreshFiles=false) sammelt sie nach
Stabilisierung ein (Safety-Net).
3. Hybrid-Pfad: Rename (Impl-Variante, kein Self-Chain) + Collect in EINER
chainPackageFileOp-Kette → atomar, kein Interleaving ueberlappender Runden.
Deferred-Pfad unangetastet (dort keine concurrent Extraktion). Regressionstest:
frische Datei wird im Hybrid-Collect deferred, vom finalen Pass gesammelt.
622 Tests gruen, tsc-Fehlerzahl unveraendert (9 pre-existing).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
User-Report: ZIP scheitert mit wrong_password -> Passwort zur Liste hinzufügen
+ Settings speichern -> "Jetzt entpacken" scheitert WIEDER -> erst nach
App-Neustart klappt "Jetzt entpacken".
Analyse (eigenständig + zweiter Analyse-Agent, gesamte TS+Java-Kette):
Renderer-Save -> updateSettings (fingerprint enthält archivePasswordList) ->
setSettings (this.settings aktualisiert) -> extractNow -> runPackagePostProcessing
-> extractPackageArchives (passwordList: this.settings.archivePasswordList) ->
archivePasswordCandidates (enthält neues PW) -> Daemon bekommt Passwörter PRO
REQUEST. Java-Daemon probiert alle PW frisch, kein Per-Archiv-Cache; zip4j/
sevenzipjbinding ohne relevanten static State. Alle Pfade propagieren die neue
Liste korrekt — statisch ist KEIN Bug auffindbar.
Verifikation per Ausschluss: die EINZIGE zustandsbehaftete Komponente, die ein
App-Neustart zurücksetzt und ein Settings-Save NICHT, ist der langlebige
JVM-Daemon-Prozess (+ der In-Memory Learned-Password-Cache). Der User bestätigt
empirisch, dass ein Neustart (= frischer Daemon) es fixt.
Fix: neue resetExtractorCachesForPasswordChange() repliziert den Neustart-Effekt
am Extractor-Subsystem — bei Änderung der Passwortliste in setSettings wird der
Learned-Password-Cache geleert und der idle JVM-Daemon heruntergefahren, sodass
die nächste Extraktion frisch mit der neuen Liste startet. Beschäftigter Daemon
wird nicht abgebrochen (laufende Extraktion bleibt unangetastet).
Diagnostik: setSettings loggt jetzt PW-Anzahl + Reset-Resultat. Zusammen mit den
bestehenden "Archiv-Passwortliste: passwordCount=N"-Logs lässt sich bei erneutem
Auftreten in 30s unterscheiden, ob das PW beim Extractor ankommt (H2, Fix greift)
oder nicht (H1, dann TS-upstream). Best-bet per Ausschluss + Logs zur finalen
Bestätigung beim nächsten Repro.
621 Tests grün, tsc-Fehlerzahl unverändert (9 pre-existing).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Problem: nach importBackup hielt der Manager weiter die STALE In-Memory-Session
(Import schrieb nur auf Disk). blockAllPersistence wurde gesetzt um Überschreiben
zu verhindern, aber nie zurückgesetzt → ignorierte der User die manuelle
"Bitte neustarten"-Aufforderung und arbeitete weiter, ging bei hartem Crash
alles verloren (stille Persistenz-Blockade).
In-Memory-Reload verworfen: aborted activeTasks settlen ASYNC und greifen in
ihren finally-Blöcken auf this.session.items[id] zu — ein Session-Swap würde
dagegen racen. Sicherer Reload bräuchte async-Refactor (alle Tasks awaiten).
Lösung: Auto-Relaunch. Nach restored===true startet main.ts die App automatisch
neu (1.5s Delay für Toast). Der frische Prozess lädt die restored Session sauber
über den bewährten Startup-Pfad — null Stale-State-Risiko. Main-getrieben (nicht
Renderer), damit ein Renderer-Fehler den Restart nicht verhindern kann.
skipShutdownPersist/blockAllPersistence schützen weiterhin das kurze Fenster +
den Quit (prepareForShutdown:5680 überspringt Persistenz sauber, fasst die stale
Session nicht an). Nach Relaunch: frischer Prozess, Flags zurückgesetzt — Footgun weg.
621 Tests grün, tsc-Fehlerzahl unverändert (9 pre-existing).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Aus der Bug-Analyse (3 Subagents): die Deferred-Post-Processing-Pipeline war
nur halb ins Abbruch-/Lifecycle-Management integriert — gleiche Ecke wie der
v1.7.156-Datenverlust.
H1: abortPostProcessing (globaler Stop/Shutdown/clearAll/external) bricht jetzt
auch packageDeferredPostProcessAbortControllers + die neue Hybrid-Map ab.
Vorher rasten MKV-Move/Cleanup/Rename gegen den synchronen Shutdown-Save.
H2: Hybrid-Post-Extract (Rename+MKV-Collect) lief als komplett ungetracktes
detached Promise. Jetzt in packageHybridPostProcessControllers (Set/Package)
registriert — SYNCHRON vor dem Promise, mit shouldAbort an beide Aufrufe.
Bewusst SEPARAT von der Deferred-Map, sonst würde runDeferredPostExtraction's
replace-Logik die laufende Hybrid-Arbeit selbst killen (Advisor-Fund).
Cancel/Reset/Stop stoppt jetzt laufende Hybrid-Verschiebungen.
M1: hasAnyDeferredPostProcessPending() — Scheduler-Abschluss + finishRun-Clear
gaten darauf. Run endet/Summary feuert nicht mehr während im Hintergrund
noch Dateien verschoben werden; Run-State wird nicht mehr mittendrin geleert.
H3: validateDownloadedFileCompletion akzeptierte 0-Byte bei source=stream-end
(kein Content-Length, keine Provider-Größe) als "fertig". Jetzt ok:false
-> bestehender download_underflow-Retry-Pfad. Verhindert leere Datei = komplett.
N1: toter (unerreichbarer) Disk-Fallback-Block in findReadyArchiveSets +
verwaiste pendingItemStatus-Map entfernt (verhaltensneutral).
Bewusst übersprungen: M2 (blockAllPersistence — vorgeschlagener Reset wäre
unsicher, In-Memory-Session ist nach Import stale) und M3 (cancelPendingAsyncSaves
— Generation-Guard schützt Korrektheit bereits). Siehe tasks/todo.md.
8 neue Tests (tests/download-completion.test.ts) inkl. H3-Regression. 621 Tests grün.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
KRITISCHER Datenverlust-Fix (Regression aus v1.7.154):
collectMkvFilesToLibrary lief seit v1.7.154 mit einem Cleanup-Loop ueber
BEIDE Source-Dirs (extractDir + outputDir). cleanupNonMkvResidualFiles
loescht alle Nicht-Video-Dateien — auf dem outputDir traf das auch die
RAR-Archive. Bei Multi-Archive-Set-Paketen (z.B. S01 + S02 RARs im selben
outputDir) wurde nach dem Extrahieren von S01 die MKV-Collection getriggert,
die dann die noch nicht entpackten S02-RAR-Parts als "Restdateien" loeschte.
Folge: S02 ging verloren (missing_file beim spaeteren Extract).
Fix: Destruktiver Cleanup (Restdateien + leere Ordner) laeuft jetzt NUR
noch auf dem cleanupDir:
- autoExtract=true -> extractDir (entpackter Inhalt, fertig verarbeitet)
- autoExtract=false -> outputDir (kein Extract, finaler Inhalt)
Der outputDir wird bei autoExtract=true nie hier aufgeraeumt — das macht
die separate Archive-Cleanup-Pipeline mit Extraktions-Guards.
Das MKV-Scannen beider Dirs (v1.7.154 Mega-Direct-.mkv) bleibt erhalten,
nur der Cleanup ist eingegrenzt.
Regressionstest verifiziert: 2 RAR-Sets im outputDir, S01-MKVs in
extractDir -> collectMkvFilesToLibrary darf S02-RARs nicht loeschen.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
UX: Beim Hinzufuegen von mega.nz Links wurden bisher nur die opaken
URL-Fragmente angezeigt ("pZl1wBRQ" etc.). Echte Filenames kamen erst
beim Mega-Debrid Unrestrict-Call, d.h. unmittelbar vor Download-Start.
Fix: Neuer src/main/mega-public-api.ts holt Filename + Groesse direkt
von Mega's Public API (g.api.mega.co.nz/cs) ohne Mega-Debrid-Quota
anzufassen. AES-128-CBC Decryption der Attribute mit dem Key aus
dem URL-Fragment.
resolveFilenames (debrid.ts) ruft den neuen Resolver fuer alle
erkannten mega.nz Links auf (concurrency 4). Auf Fehler/Rate-Limit
fallback auf den bestehenden Unrestrict-Pfad.
19 neue Tests fuer URL-Parser, AES-Decryption, Mocked-Fetch.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bug: Direct .mkv Downloads (z.B. von Mega-Debrid bei mega.nz, die
KEIN Archiv liefern) blieben mit autoExtract=true im outputDir liegen
und kamen nie in die MKV-Library. collectMkvFilesToLibrary scannte
binary nur extractDir wenn autoExtract aktiv war.
Fix: Beide Source-Dirs scannen, dedupe by basename (extractDir wins),
Safety-Check + Existenz-Check pro Dir. Cleanup-Loop läuft auch pro Dir.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Aus dem live Rename-Log + Library-Screenshot:
(1) .nfo Files landeten in der MKV-Library
moveCompanionFiles() hatte ".nfo" in der Extensions-Liste — sollte
nur fuer Subtitles gemoved werden. .nfo gehoert nicht in die
Library. Aus dem Set entfernt; renameCompanionFiles laesst .nfo
weiterhin mit-umbenennen (im Extract-Dir, harmlos), aber MKV-Move
bringt sie nicht mehr in die Library.
(2) Vollstaendige Scene-Namen wurden auf "Show.SxxExx.mkv" gekuerzt
buildSafeAutoRenameTargetPath hatte ein 247-Zeichen Total-Path-Cap,
das vollstaendige Scene-Releases wie
"Dr.House.S04E02.Der.Stoff.aus.dem.die.Heldin.ist.GERMAN.5.1.DL.AC3.720p.BDRiP.x264-TvR.mkv"
abgelehnt hat → Fallback aktiviert → "Dr.House.S04E02.mkv".
Die ABER renamePathWithExdevFallback wraps eh ueber
toWindowsLongPathIfNeeded (\?\ Prefix), und der Endpfad nach
MKV-Move ist viel kuerzer (Library-Dir-Prefix). Cap war
ueberhaupt nicht noetig und hat aktiv schoene Namen abgesaegt.
Cap entfernt; nur das 255-char NTFS Filename-Limit bleibt.
Test: neuer Test "mkv-move moves SUBTITLES to library but NOT .nfo
metadata files". 592/592 Tests gruen.
Im naechsten Schritt: Fix-Skript fuer den User damit existierende
Library-Files (mit .nfo + zu kurzen Namen) korrigiert werden ohne
Re-Download.
Independent code review fand drei echte Probleme an v1.7.151:
(a) File-stability check bei Clock-Skew rueckwaerts:
negative ageMs (mtime in der Zukunft, z.B. NTP-Korrektur, VM-Resume)
wurde von "ageMs < 2000" als "frisch" interpretiert → Datei stuck
bis Clock aufschliesst. Fix: ageMs >= 0 zusaetzlich pruefen — negativ
= "definitiv stabil".
(c) Suffix-Loop koennte Source-File als Resolved-Target waehlen:
wenn Source schon "<base>.2.mkv" heisst und das Original "<base>.mkv"
anderswo existiert, koennte die .2/.3-Loop sich selbst auswaehlen.
Fix: pathKey-Vergleich gegen sourcePath im Loop, springt weiter.
(f) xX-Format matched x264/x265/x266 Codec-Tokens:
"5x265.x265.mkv" wurde als S05E265 interpretiert.
"Movie.x264-GROUP.mkv" konnte phantome Episode triggern.
Fix: zweite Number-Group auf \d{1,2} (max 99) gecapped + negativer
Lookahead [\dx] dahinter. 3-stellige xX-Episoden (sehr selten) gehen
verloren — moderne SxxEnnn deckt das ab. Schutz gegen alle gaengigen
Codecs (x264/265/266, h264/265) und Aspect-Ratios (1920x1080).
Tests: neue assertions fuer x264/x265/aspect-ratio + 10x99 vs 10x100.
591/591 gruen.
v1.7.149 hat den Race ZWISCHEN parallelen Auto-Rename-Scans gefixt
(autoRenameInFlight). Aber es gab noch einen anderen Race:
- Hybrid-Pfad (Z.10952-66) feuert "fire-and-forget" rename->mkvMove
- Deferred-Post-Process-Pfad (Z.11672/11748) feuert "awaited" rename + mkvMove
Beide Pipes koennen GLEICHZEITIG fuer dasselbe Package laufen. Innerhalb
einer Pipe ist rename->mkvMove sequentiell, aber Pipe A's mkvMove kann
WAEHREND Pipe B's rename starten (nachdem die Rename-Serialisierung von
v1.7.149 Pipe B entsperrt hat). Resultat: Pipe A bewegt File X aus
extractDir, Pipe B's rename versucht File X umzubenennen → ENOENT, oder
File landet mit altem Hoster-Namen in der Library.
Fix: autoRenameInFlight wird zu packageFileOpChain generalisiert. Helper
chainPackageFileOp(pkgId, fn) chained beliebige file-mutierende Ops auf
das vorherige Promise. autoRenameExtractedVideoFiles benutzt es intern,
und beide collectMkvFilesToLibrary-Aufrufstellen werden jetzt explizit
durch denselben Chain geroutet.
Effekt: pro Package laeuft maximal eine post-process Operation (rename
ODER mkvMove) zu jeder Zeit, egal welche Pipe sie triggert.
Tests:
- "serializes rename and mkvMove across hybrid + deferred pipes":
4 chainPackageFileOp-Calls fuer dasselbe Package, max-concurrent == 1,
Reihenfolge erhalten, Slot nach letztem Op geleert.
- "chainPackageFileOp recovers from a failed op": Fehler im ersten Op
bricht die Chain nicht — nachfolgende Ops laufen normal weiter.
584/584 Tests gruen.
Im Production-Log:
21:30:33.957Z Auto-Rename Scan gestartet | videoFiles=25
21:30:33.992Z Auto-Rename Scan gestartet | videoFiles=25 (35ms spaeter, gleiches pkg!)
21:30:33.994Z Auto-Rename durchgefuehrt | E24.B (Scan 1)
21:30:34.009Z Auto-Rename uebersprungen: Ziel existiert (Scan 2 sieht renamed file)
21:30:34.029Z Auto-Rename durchgefuehrt | E24.A (Scan 1)
21:30:34.056Z Auto-Rename fehlgeschlagen | ENOENT (Scan 2 versucht renamed file)
Ursache: hybrid-extract feuert nach JEDEM erfolgreichen Archive einen
fire-and-forget autoRename (Z.10915), und der deferred Post-Process-Pfad
ruft am Ende nochmal autoRename auf (Z.11630). Bei einem Multi-Archive-
Package (25 Episoden) ueberlappen sich 2+ Scans auf demselben Fileset.
Ergebnis: "Ziel existiert"-Warnungen + ENOENT-Fehler beim Rename.
Manchmal blieben einzelne Files unbenannt durchrutschen (Scan 2 sieht
File X, will renamen, aber Scan 1 hat es schon weg-renamed).
Fix: pro Package via Promise-Chaining serialisieren. Neue Map
autoRenameInFlight haelt das laufende Scan-Promise pro packageId. Der
neue Wrapper kettet jeden weiteren Aufruf an das vorherige Promise an
— so laeuft maximal ein Scan zur Zeit pro Package, der naechste startet
erst wenn der vorherige fertig ist (und sieht damit den korrekten
Disk-State).
Test: zwei parallele autoRenameExtractedVideoFiles-Aufrufe fuer dasselbe
Package mit 3 obfuskierten Files. Beide resolven sauber, Summe der
Renames == 3, alle 3 Folders enthalten am Ende den korrekten Folder-
Namen statt Hoster-Obfuskation. 582/582 Tests gruen.
Regression in v1.7.147: der Folder-Override (parentEpisodeToken
ueberschreibt sourceEpisodeToken bei Mismatch) ist zu aggressiv. Bei
sauberen Scene-Releases die zufaellig im falschen Folder liegen wuerde
das den Episodennamen FALSCH umschreiben.
Beispiel aus Production-Log:
Folder: The.Royals.S01E08.Der.Grosse.stuerzt.German.DL.720p.BluRay.x264-J4F
File: the.royals.2015.s01e09.german.dl.720p.bluray.x264-j4f.mkv
Beide haben sauberes Scene-Format. Datei sagt klar S01E09 — Folder ist
falsch beschriftet (Hoster-Fehler). Die Datei zu S01E08 umbenennen
waere Daten-Korruption.
Fix: neue Helper-Funktion looksLikeObfuscatedSceneFileName() prueft ob
ein Filename Scene-Marker hat (720p/1080p, german/english, bluray/web/
hdtv, x264/x265, ac3/aac/dts) ODER 5+ Punkte als Scene-Struktur. Wenn
2+ Marker oder 5+ Punkte → KEIN Override (Source ist authoritativ).
Wenn weniger → Source ist obfuskiert, Folder gewinnt.
Beispiele aus Production:
- "awa-diethundermans02e16hd.mkv" (0 Marker, 0 Punkte) → obfuskiert,
Folder Die.Thundermans.S02E01... gewinnt → korrekt umbenannt
- "the.royals.2015.s01e09.german.dl.720p.bluray.x264-j4f.mkv"
(5 Marker) → sauber, Source bleibt → Skip statt Falsch-Rename
- "Desperate.Housewives.S01E01.German.Synced.DL.720p.WEB-DL.AC3.h264.mkv"
(5+ Marker) → sauber, kein Override
4 neue Unit-Tests fuer looksLikeObfuscatedSceneFileName, 581/581 gruen.