Fix: Post-Process-Identity-Guard (J) + Remux-Temp nie ins Library sammeln (Q)

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.)
This commit is contained in:
Sucukdeluxe 2026-06-08 22:46:14 +02:00
parent 4432fa25e8
commit 3c33b988c3

View File

@ -3546,6 +3546,11 @@ export class DownloadManager extends EventEmitter {
if (!entry.isFile()) {
continue;
}
// Never collect our own remux temp/orphan sidecars (~rd<token>.<ext>): 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<void> } = {};
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;
}