Commit Graph

794 Commits

Author SHA1 Message Date
Sucukdeluxe
36ff1c5a86 Cross-Pipe Race-Fix: Rename + MKV-Move teilen jetzt einen per-Package Lock
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.
2026-04-22 01:54:35 +02:00
Sucukdeluxe
84d02c5f98 Release v1.7.149 2026-04-22 01:38:54 +02:00
Sucukdeluxe
c417ebb57f Auto-Rename Race-Fix: parallele Scans pro Package serialisieren
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.
2026-04-22 01:38:26 +02:00
Sucukdeluxe
7ab508617a Release v1.7.148 2026-04-22 01:30:02 +02:00
Sucukdeluxe
834da04b45 Auto-Rename Folder-Override: nur bei OBFUSKIERTEM Source-Filename
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.
2026-04-22 01:29:32 +02:00
Sucukdeluxe
b1291d2e3c Release v1.7.147 2026-04-22 00:46:25 +02:00
Sucukdeluxe
19c31caab5 Auto-Rename: zwei Mismatch-Bugs gefixt (obfuskierter File-Token + weak Folder)
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.
2026-04-22 00:45:49 +02:00
Sucukdeluxe
9e165b72a3 Release v1.7.146 2026-04-21 21:36:47 +02:00
Sucukdeluxe
75036edbd1 DLC-Import Hang gefixt: kein sync-FS Log-I/O mehr pro Link
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.
2026-04-21 21:36:19 +02:00
Sucukdeluxe
56656dfef9 Release v1.7.145 2026-04-20 20:21:07 +02:00
Sucukdeluxe
90f347dc2b Startup Health-Check: proaktive Warnungen bei Problem-Zustaenden
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.
2026-04-20 20:20:25 +02:00
Sucukdeluxe
a1697e652e Release v1.7.144 2026-04-20 16:52:50 +02:00
Sucukdeluxe
5ecb636d95 Debrid-Link skip-Errors: Key bleibt "ready" statt "error"
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).
2026-04-20 16:52:18 +02:00
Sucukdeluxe
8e1159565b Release v1.7.143 2026-04-19 23:42:09 +02:00
Sucukdeluxe
d62fa548cb Debrid-Link: per-(key, host) cooldown for maxLinkHost / maxDataHost
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.
2026-04-19 23:41:07 +02:00
Sucukdeluxe
3a8be961b0 Release v1.7.142 2026-04-19 23:04:01 +02:00
Sucukdeluxe
25aa48fe99 Account-rotation logging + transient cooldown fixes
- 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
2026-04-19 23:03:22 +02:00
Sucukdeluxe
4d2f11a96d Release v1.7.141 2026-04-19 22:12:06 +02:00
Sucukdeluxe
cdec0029fe Debrid-Link stability: stop double-blocking, shorter transport cooldown, max-wins
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>
2026-04-19 22:11:35 +02:00
Sucukdeluxe
7be2d2e148 Release v1.7.140 2026-04-19 21:59:59 +02:00
Sucukdeluxe
c3590f08fc Fix stale-state when account credentials change at runtime
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>
2026-04-19 21:59:28 +02:00
Sucukdeluxe
04413599d8 Release v1.7.139 2026-04-19 14:11:16 +02:00
Sucukdeluxe
4d1f3c3fdc Performance: hash-based IPC state diffing (the big one)
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>
2026-04-19 14:10:48 +02:00
Sucukdeluxe
ca47773317 Release v1.7.138 2026-04-19 13:57:42 +02:00
Sucukdeluxe
67fe689e28 Performance: cache cloneSettings() between snapshots (400ms TTL)
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>
2026-04-19 13:57:15 +02:00
Sucukdeluxe
ce33617aa6 Release v1.7.137 2026-04-19 13:47:24 +02:00
Sucukdeluxe
d71dd3af0b Performance: visiblePackages dep fix + ItemRow date memo + small Object.keys cuts
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>
2026-04-19 13:46:57 +02:00
Sucukdeluxe
b92330863a Release v1.7.136 2026-04-19 13:31:08 +02:00
Sucukdeluxe
5b4ad99923 Performance: cloneSession shallow refs + scheduler 1-pass + speed obj alloc
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>
2026-04-19 13:30:42 +02:00
Sucukdeluxe
459d078cb0 Release v1.7.135 2026-04-19 13:08:08 +02:00
Sucukdeluxe
3c9894c7b0 Performance: ItemRow extraction + scheduler single-pass + selectedIds memo fix
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>
2026-04-19 13:07:42 +02:00
Sucukdeluxe
c1edb07009 Release v1.7.134 2026-04-19 12:33:07 +02:00
Sucukdeluxe
bece2f3e85 Performance: prune long-lived caches, hoist regexes, idle chart redraws
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>
2026-04-19 12:32:38 +02:00
Sucukdeluxe
26edc79784 Release v1.7.133 2026-04-19 11:53:37 +02:00
Sucukdeluxe
d4b98ad172 Better error logging for non-Administrator/headless server scenarios
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>
2026-04-19 11:53:07 +02:00
Sucukdeluxe
f44a321e74 Release v1.7.132 2026-04-14 15:03:33 +02:00
Sucukdeluxe
6e936cd5bc Bonus dir detection: normalize separators (Making.Of, Behind.The.Scenes)
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>
2026-04-14 15:03:02 +02:00
Sucukdeluxe
9f59b6e7ca Release v1.7.131 2026-04-14 11:46:15 +02:00
Sucukdeluxe
1dfb486145 Auto-rename safety net: never strip valid SxxExx episode token
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>
2026-04-14 11:45:41 +02:00
Sucukdeluxe
90473b13cb Release v1.7.130 2026-04-14 11:29:18 +02:00
Sucukdeluxe
6713771144 Skip bonus/extras content in MKV collection and auto-rename
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>
2026-04-14 11:28:44 +02:00
Sucukdeluxe
16a59acaef Release v1.7.129 2026-04-04 21:28:51 +02:00
Sucukdeluxe
49efebd001 Extend hybrid companion stem matching to all archive types
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>
2026-04-04 21:28:06 +02:00
Sucukdeluxe
ee69dcf4cc Release v1.7.128 2026-04-04 20:42:20 +02:00
Sucukdeluxe
711147fc10 Fix companion files stuck at extraction labels in hybrid mode
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>
2026-04-04 20:41:38 +02:00
Sucukdeluxe
85889de790 Release v1.7.127 2026-04-04 20:32:36 +02:00
Sucukdeluxe
ab5fcaf836 Clean up companion metadata files (.sfv, .nfo, .md5) with their archives
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>
2026-04-04 20:31:58 +02:00
Sucukdeluxe
da9417c4f7 Release v1.7.126 2026-04-04 20:21:52 +02:00
Sucukdeluxe
021401e3b6 Mark companion metadata files (.sfv) as extracted during hybrid extraction
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>
2026-04-04 20:21:16 +02:00
Sucukdeluxe
68bfeb574f Release v1.7.125 2026-04-04 20:11:55 +02:00