From 3c33b988c30b55e939e072b73b8128a05f982a34 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Mon, 8 Jun 2026 22:46:14 +0200 Subject: [PATCH] Fix: Post-Process-Identity-Guard (J) + Remux-Temp nie ins Library sammeln (Q) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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.) --- src/main/download-manager.ts | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index b05905e..5222f22 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -3546,6 +3546,11 @@ export class DownloadManager extends EventEmitter { if (!entry.isFile()) { continue; } + // Never collect our own remux temp/orphan sidecars (~rd.): a + // partial file left by a crash mid-remux must not be swept into the library. + if (entry.name.startsWith("~rd")) { + continue; + } const extension = path.extname(entry.name).toLowerCase(); if (!normalizedExtensions.has(extension)) { continue; @@ -6107,6 +6112,11 @@ export class DownloadManager extends EventEmitter { } private dropItemContribution(itemId: string): void { + // NOTE: deliberately does NOT subtract from session.totalDownloadedBytes / + // sessionDownloadedBytes. Those are cumulative-session counters and must stay + // put when a completed item is removed from the queue (see the test "keeps + // cumulative session totals when completed items are removed from the queue"). + // The retry path subtracts on its own because those bytes get re-downloaded. this.itemContributedBytes.delete(itemId); this.invalidateStatsCache(); } @@ -7151,6 +7161,9 @@ export class DownloadManager extends EventEmitter { const abortController = new AbortController(); this.packagePostProcessAbortControllers.set(packageId, abortController); + // Holder so the task's own finally can identity-check itself (the task Promise + // cannot reference its own const inside its initializer). Assigned right after. + const handle: { task?: Promise } = {}; const task = (async () => { const slotWaitStart = nowMs(); await this.acquirePostProcessSlot(packageId); @@ -7197,8 +7210,16 @@ export class DownloadManager extends EventEmitter { } while (this.hybridExtractRequeue.has(packageId)); } finally { this.releasePostProcessSlot(); - this.packagePostProcessTasks.delete(packageId); - this.packagePostProcessAbortControllers.delete(packageId); + // Identity guard: only clear the map entries if they still point to THIS + // task/controller. After an abort deletes our handle a new run can install + // a fresh task+controller for the same packageId; a blind delete here would + // orphan that newer task (uncancellable) and allow a duplicate concurrent run. + if (this.packagePostProcessTasks.get(packageId) === handle.task) { + this.packagePostProcessTasks.delete(packageId); + } + if (this.packagePostProcessAbortControllers.get(packageId) === abortController) { + this.packagePostProcessAbortControllers.delete(packageId); + } this.persistSoon(); this.emitState(); if (this.hybridExtractRequeue.delete(packageId)) { @@ -7209,6 +7230,7 @@ export class DownloadManager extends EventEmitter { } })(); + handle.task = task; this.packagePostProcessTasks.set(packageId, task); return task; }