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; }