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.
Bug 1 - Obfuskierter Datei-Token vs Folder:
Hoster verteilen Episoden in EPISODEN-Ordner ABER scrambeln den
Datei-Token zur Anti-Piracy:
Folder: Die.Thundermans.S02E01.Der.Thunder.Van.GERMAN...
File: awa-diethundermans02e16hd.mkv (Datei sagt E16, Folder E01)
Bisher hat der Episode-Token-Mismatch-Check uebersprungen → Datei
behaelt obfuskierten Namen → MKV-Move kopiert Garbage-Namen ins
Library-Verzeichnis.
Fix: wenn der unmittelbare Eltern-Ordner explizit denselben SxxExx-
Token wie unser computed targetBaseName traegt (also ein Per-Episode
Scene-Folder ist, NICHT der Extract-Root), wird der Folder-Token als
authoritativ behandelt — Scene-Releases benennen Episoden-Folder
deterministisch korrekt, der Datei-Name ist die Obfuskation.
Bug 2 - Weak Folder ueberschreibt perfekten Source-Filename:
Source: Desperate.Housewives.S01E01.German.Synced.DL.720p.WEB-DL.AC3.h264
Folder-Kette: ["S01 Complete", "Desperate.Housewives.S01.Synced..."]
Auto-Rename hat den unmittelbaren Parent "S01 Complete" gewaehlt und
daraus "S01E01 Complete" gebaut — kompletter Verlust des Series-Namens.
Fix: neue Helper-Funktion hasMeaningfulSeriesPrefix() prueft, ob ein
Name mindestens 3 alphabetische Zeichen VOR dem Season-Token hat.
Wenn (a) Source einen Episode-Token hat, (b) Source einen Series-Prefix
hat, (c) Computed Target KEINEN Series-Prefix hat und (d) Target weniger
als die Haelfte der Source-Laenge ist → Source behalten, Rename
ueberspringen. Renaming wuerde Information ZERSTOEREN.
Tests: 3 neue Unit-Tests fuer hasMeaningfulSeriesPrefix decken die
relevanten Faelle ab (echte Series-Namen vs generische Season-Labels
vs. Folder ohne Season-Token). 577/577 Tests gruen.
Symptom: Nutzer zieht DLC mit vielen Paketen rein, App haengt 1-2 min.
Ursache: addPackages() rief logPackageForItem() pro Link auf. Jede
dieser Calls triggerte ~10 synchrone FS-Operationen:
- ensurePackageLog: mkdirSync + existsSync
- ensureItemLog: mkdirSync + existsSync + writeFileSync (first-time)
+ 2× appendFileSync (first-time header)
- logPackage + writeItemLogEvent (appendFileSync, batched)
Bei einer DLC mit 60 Paketen × 25 Links = 1500 Items → ~9.000-15.000
sync FS-Calls. Auf langsamen Disks / Netzwerk-Shares: 60-120 Sekunden
Event-Loop-Blockade. UI eingefroren.
Fix: per-Item-Logs waehrend Bulk-Add nicht mehr initialisieren. Sie
werden lazy beim ersten echten Lifecycle-Event (Download-Start, Fehler)
angelegt. Stattdessen EINE zusammengefasste "Links registriert (N)"
Zeile ins Package-Log pro Paket — bei >50 Links mit gekuerzter
Vorschau (erste 20 + "+N more") damit die Log-Zeile nicht riesig wird.
Neuer Test "bulk-adds large DLC containers without initializing per-item
logs" verifiziert: 1500 Items werden in <5s hinzugefuegt (lokal unter
300ms), keine Item-Log-Dateien entstehen, pro Paket existiert genau ein
Package-Log.
Laeuft einmal beim App-Start und warnt klar im Log, wenn etwas auffaellt
— BEVOR der Nutzer mitten im Download stolpert. Blockiert den Start
nicht, schreibt nur in rd_downloader.log + audit-log.
Pruefungen:
- Download-Ziel-Ordner fehlt / nicht beschreibbar / nicht konfiguriert
- Runtime-Ordner (%APPDATA%/runtime) fehlt oder nicht beschreibbar
- Wenig Festplattenplatz im Download-Ordner (< 5 GB)
- Kein einziger Debrid-Provider konfiguriert → Downloads koennen nicht
funktionieren
- State-Datei > 50 MB (alte abgeschlossene Pakete sollten geprunt werden)
Listet zudem als INFO alle aktiv konfigurierten Provider auf, damit aus
dem Startup-Log klar ist was aktiv ist (Mega-Debrid X Accounts,
Debrid-Link X Keys, etc.).
Reine Funktion runStartupHealthCheck() → HealthCheckReport, 6 Unit-Tests
decken die wichtigsten Pfade ab. Wiring in AppController-Constructor ist
in try/catch — falls der Check selbst abstuerzt, stoert das den Start
nicht.
fileNotAvailable, disabledServerHost, notFreeHost, serverNotAllowed,
freeServerOverload, maintenanceHost, noServerHost sind LINK- oder
HOST-level Fehler, nicht Key-level. Der Key antwortet ganz normal und
sagt nur "diesen Link kann ich aktuell nicht verarbeiten".
Vorher wurde trotzdem der Runtime-Status auf "error" gesetzt — sah in
der UI aus als waere der Key kaputt und hat die Rotations-Heuristiken
irritiert.
Fix: bei failure.category === "skip" den Runtime-Status in Ruhe lassen.
Der Key bleibt "ready" (bzw. was er vorher war). Invalid bleibt
"invalid", alle anderen fehlerhaften Antworten bleiben "error".
Test: Key 1 gibt fileNotAvailable zurueck → Key 2 erfolgreich. Key 1
darf danach NICHT "error" sein (per neuem Test-Helper
getDebridLinkKeyRuntimeStateForTests).
Previously, when a Debrid-Link key returned maxDataHost or maxLinkHost
("you've used up YOUR per-host quota for this hoster on this key"), the
WHOLE key got a 2-min key-wide cooldown — blocking it for all hosters
even though it was only exhausted for that one host.
Now those errors apply a per-(key, host) cooldown instead:
- Key 1 hits maxDataHost on rapidgator → only (Key 1, rapidgator) is
blocked. Key 1 stays usable for uploaded.net etc. in the same rotation
- Same key + same host on subsequent attempts: skipped with explicit
"Host-Cooldown rapidgator bis HH:MM:SS" log line
- Key-wide quotas (maxLink, maxData) still apply key-wide as before
Implementation:
- DEBRID_LINK_QUOTA_ERRORS split into key-wide vs host-only sets
- New debridLinkKeyHostCooldowns map keyed by `${keyId}|${hoster}`
- setDebridLinkKeyHostCooldownState mirrors max-wins / strong-category
semantics of the per-key version, falls back to key-wide cooldown when
the hoster can't be parsed (safer than thrashing)
- Key runtime status stays "ready" on host-only failures — only this
(key, host) is blocked, the key is still healthy for other hosters
- Reset/prune helpers (resetDebridLinkRuntimeStateForTests,
pruneDebridLinkRuntimeStateForKeys, pruneExpiredDebridLinkRuntimeState)
clear the new map too
- New rotation log event SKIP_HOST_COOLDOWN
Test: 2 keys, key1 hits maxDataHost on rapidgator → key2 succeeds.
Second rapidgator request: key1 SKIPPED via host-cooldown.
Third request to uploaded.net: key1 tried again and succeeds.
- New dedicated account-rotation.log (audit-style) so multi-account/key
rotation flow is visible without rd_downloader.log noise
- Mega-Debrid: always show "Account X/Y (masked@login)" label even with
one account, and log a clear "TESTE Account fuer Link-Generierung..."
line BEFORE every network call so the user sees which account is in
play even if the call hangs
- Mega-Debrid: on FAILED, log which account will be tried next
(skipping disabled/limited/cooldown ones), so the rotation is auditable
- Mega-Web "Antwort leer" / empty body now uses 20s cooldown instead of
120s — empty responses are typically transient server hiccups, not
real failures (caused healthy accounts to be unfairly blocked)
- Generic transport/unknown errors: 30s cooldown instead of 120s
- Global stall watchdog no longer counts items in "validating" status —
per-item validating watchdog already handles them and gives the
multi-account rotation enough time. Without this fix the global
watchdog could abort the unrestrict mid-rotation (e.g. account 3 of 3
still being tested) just because no download bytes had arrived
- Same logging treatment applied to Debrid-Link key rotation
User reported Debrid-Link "often jumps into provider-cooldown" and feels
unstable. Root cause was four cooperating bugs that turned isolated key-level
failures into provider-wide multi-minute outages.
Fix 1: Skip provider-wide circuit breaker for ALL Debrid-Link errors
(download-manager.ts ~8689)
Previously only the explicit `debrid_link_cooldown:` sentinel was bypassed;
every other Debrid-Link error (terminal failures, timeouts, parse errors)
still went through recordProviderFailure() + applyProviderBusyBackoff(),
applying a provider-wide cooldown ON TOP of the per-key cooldown debrid.ts
already managed. Now any error message containing "debrid-link" or where
the failure key is "debridlink" skips the provider-level circuit breaker
entirely. Per-key cooldowns alone are the right granularity.
Fix 2: Transport errors get a short 15s cooldown, not 2 min
(debrid.ts ~2684)
A single network timeout / ECONNRESET was parking the key for 2 full
minutes. With 9 keys all of which might experience the same transient
issue at different moments, this could cascade into all keys cooling
down for 2 min each. Now isolated transport hiccups get 15s while real
API/server problems still get the full 2 min.
Fix 3: HTTP 200 with success:false (no error code) is now temporary, not fatal
(debrid.ts ~2691)
Previously these went through to the fallthrough "fatal: true" which
permanently failed the item. Now they get a 30s temporary cooldown and
the item retries on the next key.
Fix 4: setDebridLinkKeyCooldownState is max-wins under concurrent calls
(debrid.ts ~135)
When 8 parallel items all hit the same key with floodDetected, each
computes its own cooldown duration and calls setDebrid LinkKeyCooldown
State. Without max-wins, the LAST setter could shorten the cooldown
(e.g. one item read a 1h Retry-After header, another defaulted to 2 min;
the 2 min would then overwrite the 1h). Now the longer cooldown wins,
with rate_limit/quota/invalid categories also winning over plain
temporary regardless of duration.
Tests: 201/201 (debrid + download-manager) green.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reported user bug: "When I add an account, remove it, add a new one, the
new account is only really used after restart." Multiple cache layers were
not invalidated when settings changed, causing the system to keep using
stale state until the app was restarted.
Three layers of caching needed invalidation:
1. DebridService cached client instances (debrid.ts ~3067)
The cached DebridLinkClient / LinkSnappyClient / DdownloadClient hold
internal state (session cookies, auth tokens, parsed key lists). Without
explicit invalidation when credentials change, the OLD client instance
keeps serving requests until apiKeysRaw / login+password no longer match
the cache key - which doesn't always trigger because the cache key may
incidentally match (e.g. user removes and re-adds the same key).
Fix: setSettings() now compares previous vs next credentials per provider
and explicitly clears the cache when they differ.
2. MegaDebridClient.cachedApiTokens (debrid.ts ~1533)
Static module-level token cache keyed by login (lowercase). When a user
changes the password for an existing login (same login, new password),
the cached token was kept for up to 20 minutes and would only get cleared
after the API returned 401/403.
Fix: Two new static methods - pruneCachedTokensNotIn() removes entries
whose login is no longer in the active list, and clearCachedApiToken()
force-clears a specific login. Both called from setSettings() based on
diff between previous and next account list.
3. Module-level Debrid-Link cooldown maps (debrid.ts ~41-51)
debridLinkKeyCooldowns / debridLinkKeyCooldownDetails / runtime statuses
are keyed by API key ID (FNV-1a hash of the key). When a key was put
into cooldown then removed from settings then re-added later, the old
cooldown entry would still block it.
Fix: New pruneDebridLinkRuntimeStateForKeys() function called from
setSettings() removes cooldown entries for keys no longer in the active
set.
4. providerFailures circuit-breaker map (download-manager.ts ~1990)
Per-provider failure tracking with cooldownUntil. When a user removes a
failing account and adds a new one of the same provider, the cooldown
would carry over. Now setSettings() compares per-provider credentials
and clears matching providerFailures entries when they change.
Reproduction (now fixed):
1. Add Debrid-Link key A
2. Trigger a failure (e.g. invalid link) so A goes into cooldown
3. Remove A, add B in settings
4. Try a download immediately - previously the cooldown for A or the
cached client with A's state could still prevent B from being used.
After this fix B is used immediately.
Tests: 201/201 (debrid + download-manager) green.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Implements per-item / per-package hash-based diffing for the IPC state-update
channel. This is the architecturally biggest performance win — for queues
with thousands of items where most are idle between emits, this can cut
IPC payload size by 80-95%.
How it works:
1. New `getSnapshotForEmit()` method computes a compact hash per item and
per package covering the visible/mutable fields. On each emit it includes
only items/packages whose hash changed since the last emit, plus a list
of removed IDs. Every 30 seconds a full resync is sent for safety.
2. A new `payloadKind: "full" | "delta"` field on UiSnapshot signals the
format. `removedItemIds` and `removedPackageIds` lists carry deletions.
3. The renderer maintains a `masterSnapshotRef` and merges incoming deltas:
spreads new items over master items, deletes the removed-IDs, then sets
the merged snapshot as React state. Full payloads replace the master
entirely (initial sync + 30s resync).
4. The existing direct `getSnapshot()` API used by app-controller, debug-server,
and link-export is unchanged — they still get a full snapshot. Only the
"state" emit channel uses delta encoding.
Trade-offs accepted:
- Hash computation cost: ~13 string concats per item per emit. With 5000
items at 700ms intervals that's ~7100 hash ops/sec — well under 1ms total.
- The 30s full resync ensures any drift bug self-heals within 30s without
user-visible glitch.
- Server keeps two extra Maps (item/package hash tracking).
Items / packages that are completely idle between emits add ZERO bytes to
the IPC payload now, instead of ~450 bytes per item. For a normal queue of
5000 items where ~30 are actively downloading, payload drops from ~3.6 MB
to ~30 KB per emit — a 100x reduction.
Tests: 140/140 download-manager + 133/133 storage+auto-rename green.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
cloneSettings() copies 85+ fields including 6 nested usage Maps and the
bandwidth schedule array. With ~700ms emit interval most snapshot ticks
clone identical settings. Add a time-based cache (400ms TTL) so most
snapshots reuse the previous clone.
Settings are mutated in-place by hot paths (provider usage tracking, debrid
key counters), so a reference check wouldn't catch changes — but the 400ms
TTL window is short enough that user-visible setting changes still appear
within one render cycle. replaceSettings() explicitly invalidates the
cache for immediate visibility on user setting changes.
The 4 architecturally-invasive items I considered for this round —
recordSpeed batching (already 120ms-bucketed, false alarm), full IPC state
diffing (392 mutation sites, too risky), list virtualization (variable
package heights make it complex), and per-channel settings/stats split
(invasive type changes) — are deferred. This caching change captures most
of the practical settings-clone savings without the architectural risk.
All 140 download-manager tests green.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Five more low-risk hot-path optimizations:
1. visiblePackages no longer re-runs the sort callback on every item update.
The sort is only meaningful when running && autoSort && >1 packages, so we
pass null as items dep otherwise. Previously fired the full O(N) sort
pass on every progress tick even when it would have returned the input
array unchanged.
2. ItemRow memoizes formattedCreatedAt + displayStatus + statusTitle so a
row that re-renders because of progress/speed changes no longer pays for
formatDateTime() and computeDisplayedItemStatus() twice (title+body).
3. resetSessionTotalsIfQueueEmpty: removed redundant Object.keys() check.
itemCount + packageOrder.length cover the same condition without
allocating two intermediate arrays.
4. markQueuedAsReconnectWait: replaced Object.keys() array allocation with
for-in iteration when runItemIds is empty. Saves a 5000-element string
array allocation every 900ms during reconnect.
5. columnOrderJson useEffect dep: replaced JSON.stringify with cached
join() + useMemo. Stringifying a 7-element settings array on every
render was ~2-3ms per render for nothing.
All 140 download-manager tests green. Renderer + main both build clean.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three deeper optimizations focused on hot allocations:
1. cloneSession(): items/packages references shared instead of per-item
shallow clone. The IPC layer runs structuredClone() in the same tick
so the renderer always gets an isolated copy; in-process consumers
read snapshots synchronously without mutating. Eliminates ~5000
object allocations per emit on a 5000-item queue.
2. findNextQueuedItem(): single-pass priority scan instead of 3 separate
passes (high → normal → low). Returns immediately on high-priority
match; collects best normal/low candidate while iterating. Saves up
to 2x O(n) iterations per scheduler tick.
3. packageSpeedBps: direct loop assembly instead of
Object.fromEntries([...Map].map(...)) (3 allocs per entry → 1).
Idle case now returns a stable EMPTY_PACKAGE_SPEED_BPS reference
so the renderer's useMemo on it doesn't recompute on every snapshot
while the queue is paused/stopped.
All 565 tests green.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Major optimizations to reduce UI lag with large queues (5000+ items):
1. ItemRow extracted to its own memoized component (renderer)
Previously every package re-render mapped all its items inline,
producing N×M re-renders per state update. Now each item-row only
re-renders when ITS specific data changes, with custom equality on
the visible fields (status, progress, speed, fullStatus, etc.).
Also adds stable useCallback handlers per item.
2. PackageCard stats consolidated into single useMemo (renderer)
Replaces 5 separate filter()/some() + 2 reduce() calls (O(7N)) with
one O(N) pass collecting all aggregates (done/failed/cancelled/
extracted/extracting/activeProgress/extractingProgress).
3. selectedIds memo comparator fixed (renderer)
Custom equality now checks if selection state changed for items in
THIS package only. Previously any selection anywhere broke memo on
all 200+ visible PackageCards.
4. Scheduler single-pass queue presence (main)
New getQueuePresence() returns hasImmediate + hasDelayed in one
iteration. Replaces hasQueuedItems() + hasDelayedQueuedItems() that
each scanned packages independently. Saves one full O(n) iteration
per scheduler tick.
No functional changes. All 565 tests green.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three low-risk optimizations that reduce CPU/memory footprint without
changing user-visible behavior:
1. Periodic cleanup of unbounded module-level Maps (24/7 stability):
- debridLinkKeyCooldowns, debridLinkKeyCooldownDetails,
debridLinkKeyRuntimeStatuses (debrid.ts)
- megaDebridAccountCooldowns (debrid.ts)
- allDebridHostInfoCache (download-manager.ts)
- All pruned every 10 min via existing resetStaleRetryState() with a
1h grace window for debugging
- Without this, modules accumulated entries indefinitely over days
of continuous server operation
2. Hoist regex literals in resolveArchiveItemsFromList() to module scope.
Avoids 5 RegExp constructions per call. The function is called per
archive set during extraction discovery — adds up on packages with
many archives.
3. BandwidthChart: skip the 250ms redraw interval while the session is
stopped or paused. The chart renders once on state change to show the
latest history, then sleeps until downloads resume. Saves 4 canvas
redraws per second when idle (renderer CPU).
No functional changes. All 565 tests green.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three improvements for users running on servers where the Windows account
is not "Administrator" or where the environment is headless (Service, RDP-
disconnected, no interactive desktop):
1. readSettingsFile / readSessionFile: distinguish ENOENT (normal first run)
from EACCES/EPERM (permission problem). The latter logs an explicit
message including the current Windows username so the user can spot
misconfigured ACLs immediately.
2. ensureBaseDir: log EACCES/EPERM with the current username before re-
throwing. Previously the error bubbled up without any hint why the
AppData directory creation failed.
3. createTray: log a warning when Tray creation fails (typical on Windows
Service / headless servers / RDP disconnected sessions). Previously the
error was silently swallowed and minimize-to-tray would just not work
without explanation.
These errors were silently swallowed before, making it impossible to
diagnose problems on servers with restricted user accounts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The v1.7.130 BONUS_DIR_PATTERNS used substring matching with space-separated
patterns like "making of" and "behind the scenes", but real-world subfolder
names use dot/dash/underscore separators (e.g. "Breaking.Bad.S05.Making.Of").
These were NOT detected as bonus dirs, causing the safety net in v1.7.131 to
apply the source filename's episode token to the package name, producing
mislabeled bonus files like "Breaking.Bad.S05E10.GERMAN.BluRay.720p.TSCC".
Fix: normalize folder segments by stripping all separators ([._-\s]+) before
matching against BONUS_DIR_NORMALIZED_PATTERNS. "Breaking.Bad.S05.Making.Of"
normalizes to "breakingbads05makingof" which matches "makingof".
Also extend BONUS_FILENAME_RE with "inside-e\d+" and "making-of-e\d+" to
catch more filename variants from Breaking Bad BluRay extras.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Real-world scenario from user logs: package "Drei.Meter.ueber.dem.Himmel.
S01GERMAN.DL.720P.WEB.X264-WAYNE" (note malformed S01GERMAN with no
separator) caused the auto-renamer to strip the source's S01E01..S01E08
episode tokens because SCENE_SEASON_ONLY_RE doesn't match a season followed
by an immediate letter (no separator).
Result: all 8 episodes in the season pack collapsed to the same target name
and collided in the MKV library with (2)(3)(4)(5)(6)(7)(8) suffixes.
Fix: After buildAutoRenameBaseNameFromFoldersWithOptions, check if the
source filename has a valid episode token. If yes:
1. If target has NO episode token: try to insert it via regex replacement
(Sxx<garbage> -> SxxExx.<garbage>), then via applyEpisodeTokenToFolderName.
If both fail, skip the rename entirely (preserve source name).
2. If target has a DIFFERENT episode token: skip the rename (mislabel risk).
This guard is the last line of defense against the helper's regex
limitations on malformed package names.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Bonus content (Featurettes, Behind-The-Scenes, Making-Of, Deleted Scenes,
etc.) was being moved into the flat MKV library with generic names like
"Schrotflinte.mkv" or "White.House.mkv", losing all show context. Auto-rename
also touched these files and would mislabel them with episode tokens.
Real-world impact: 397 bonus files from Breaking Bad S03/S04/S05 BluRay
extras subdirectories landed in the user's main library with nonsense names.
Fix:
- Add isInsideBonusDir() that walks the path from file to package root,
checking each directory segment for bonus indicators (Extras, Bonus,
Featurettes, Specials, Behind-The-Scenes, Making-Of, Deleted-Scenes, etc.)
- Add BONUS_FILENAME_RE to catch bonus indicators in filenames (making-of-e02,
deleted-scene, alternate-ending, gag-reel, behind-the-scenes, etc.)
- Auto-rename: skip files matching either pattern
- MKV collection: skip files matching either pattern, log skipped count
Bonus files now stay in the package output directory with their original
names; only the actual episodes get moved to the flat library.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The stem extraction regex for matching companion metadata files (.sfv,
.nfo) to their archives only handled RAR patterns. Now also covers
ZIP, 7z, tar, generic splits, and recovery volumes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When all archive parts were already extracted in a prior hybrid round and
a companion file (.sfv, .nfo, .md5) downloads later, result.extracted is
0 so the companion's status was never updated from "Entpacken - Ausstehend"
or "Entpacken - Warten auf Parts".
Now companion metadata files are explicitly marked as "Entpackt (Metadaten)"
when no archives need extraction and no failures occurred, preventing them
from blocking package completion indefinitely.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
collectArchiveCleanupTargets() now includes companion files (.sfv, .nfo,
.md5, .sha1, .sha256, .crc, .srr) that share the same base stem as the
archive parts. Previously these were left as orphans after archive cleanup.
Applies to all archive types: multipart RAR, single RAR, ZIP, split ZIP,
7z, and split 7z.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SFV files belong to the same archive set as the RAR parts but were not
included in hybridFileNames, causing them to stay stuck on "Entpacken -
Ausstehend" after the RAR parts were successfully extracted. This blocked
package completion in hybrid extraction mode.
Fix: collect archive base stems from extracted parts and match companion
metadata files (.sfv, .nfo, etc.) by stem, adding them to hybridFileNames
so they get marked as "Entpackt" together with their archive parts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
SFV checksum verification files are legitimately tiny (~128 bytes) but were
rejected by the "suspicious small download" detection, causing infinite
"Direktlink erneuern" retry loops that blocked package extraction.
- Add KNOWN_SMALL_FILE_RE for .sfv, .nfo, .nzb, .md5, .sha1, .sha256, .crc,
.txt, .url, .lnk, .srr file extensions
- Skip suspicious-small-download rejection for known small files when they
match their expected size (or have no size expectation)
- Skip tiny-download error detection for known small metadata files
- Add test: verifies .sfv file downloads without retries and completes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add safeJsonReplacer to all JSON.stringify calls in storage.ts to prevent
NaN/Infinity values from corrupting state files and causing queue loss
- Fix LinkSnappy and 1Fichier retry loops: use sleepWithSignal() instead of
sleep() so abort signals are respected during retry delays
- Fix Debrid-Link polling: replace raw setTimeout with sleepWithSignal() so
URL generation polling can be cancelled
- Fix Mega-Debrid doConnectApi: clear token cache on 401/403 responses
instead of caching invalid credentials for 20 minutes
- Add logging when normalizeLoadedSession removes orphaned items so data
loss during startup is visible in logs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Increase quit timeout from 900ms to 5000ms to ensure pending saves complete
- Add persistNowSync() called before update install to flush queue to disk
- Remove blockAllPersistence from shutdown save condition — shutdown must
always persist to prevent data loss across restarts
- Add temp file recovery as last resort when both primary and backup
session files are corrupted
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Some hosters/debrid services obfuscate downloaded archive filenames by
mutating characters and changing extensions (e.g. .part06.rar → .part06.mov,
star_crossed → star_crossfed). This breaks extraction since the extractor
relies on filename patterns to discover archive parts.
New deobfuscateArchiveFiles() method runs after download, before extraction:
- Reads magic bytes of non-archive files via detectArchiveSignature()
- If RAR/7z/ZIP signature found: corrects the extension
- Uses correctly-named sibling .rar files as reference to reconstruct
the full correct filename including part number
- Updates item.fileName and item.targetPath after rename
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- notDebrid (host-level) no longer burns all keys: stops rotation immediately
with 5min cooldown instead of cycling through all 9 keys pointlessly
- Remove double provider-blockade: debrid_link_cooldown no longer stacks
recordProviderFailure + applyProviderBusyBackoff on top of key cooldowns
- Detect timeout cascades: 2+ consecutive transport failures trigger 3min
cooldown instead of burning remaining keys
- Case-sensitive rename: files with different casing (e.g. lowercase scene
names) now get properly renamed instead of being skipped as "already matching"
- Extended sample filter: detect -s.mkv suffix and \Sample\ subdirectories
in auto-rename (already worked in MKV-move)
- Add key status display with state pills in Debrid-Link key stats popup
- Add parseDebridLinkTerminalFailure for fast-fail on exhausted keys
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add polling loop (5x 2s) in resolveDownloaderEntry when /add returns
no downloadUrl — Debrid-Link sometimes needs seconds to generate links
- Classify missing/expired downloadUrl as temporary instead of fatal so
key rotation kicks in before giving up
- Change notDebrid from fatal to temporary — "host may be down" is
transient, all keys should be tried before failing
- Raise parseRetryAfterMs cap from 2min to 1h — floodDetected mandates
"retry after 1 hour" per API docs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Remove daily-log module entirely (caused UI freezes due to sync I/O
even after async rewrite). Revert queue-scope stop() change (was for
a different project). All source files now match v1.7.112.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Rewrite daily-log from synchronous fs.writeSync to async buffered
writes (500ms flush interval), matching the main logger's pattern.
The sync writes blocked the event loop on every log line.
- Revert the stop() queue scope change from v1.7.114 which was
intended for a different project.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When startItems() was used with a subset of items (e.g. 2000 of 6020),
stopping and restarting would pick up ALL 6020 queued items instead of
just the original 2000. Now stop() marks items outside the run set as
"Gestoppt" so they are not automatically included in the next start().
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All log output is now additionally written to daily log files:
daily-logs/YYYY-MM/YYYY-MM-DD.log (main log)
daily-logs/YYYY-MM/YYYY-MM-DD-rename.log (rename log)
Automatic cleanup of daily logs older than 30 days. The existing
rd_downloader.log and rename.log continue to work as before.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The post-MKV-move rename pass added in v1.7.107 ran on the shared
mkvLibraryDir (Entpackt/), causing files from OTHER packages to be
renamed to the current package's name. For example, Orange.Is.The.New.Black
files were renamed to Ted.S02E13...SAUERKRAUT.mkv.
Remove the post-MKV-move rename entirely. The original hybrid race
condition (1 file per season not renamed) is far less damaging than
cross-package corruption.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the raw login:password textarea with a proper form-based UI:
- Individual Login + Password input fields with "Hinzufügen" button
- List of configured accounts with masked logins and "Entfernen" button
- Duplicate login detection
- Storage format unchanged (megaCredentials stays login:password pairs)
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Multiple Mega-Debrid accounts can now be configured as login:password
pairs (one per line). When an account hits Fair-Use limits or errors,
the next account is tried automatically.
- New parser module mega-debrid-accounts.ts (parse, ID generation,
masking, serialization)
- Per-account daily limits, usage tracking, enable/disable
- Account rotation with per-mode cooldowns (API failures don't
block Web attempts)
- Backward compatible: existing single megaLogin/megaPassword
is auto-migrated to the new format
- UI: textarea for credentials, account list with masked logins
Follows the existing Debrid-Link multi-key pattern.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Scene releases occasionally use SxxSxx (e.g. s05s01) instead of
SxxExx — the second S is a typo for E. Add a fallback regex to
detect this pattern and correctly interpret it as S05E01.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sample files like wayne-sample.mkv were renamed by auto-rename which
stripped the -sample suffix. After rename they were indistinguishable
from the main MKV, causing MKV collection to create (2) copies
(e.g. Messiah.Superstar.S01E01...WAYNE (2).mkv at 17 MB alongside
the real 470 MB episode).
Auto-rename now skips files with a "sample" token in their name,
matching the same detection used by MKV collection's sample filter.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>