diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 1e83c3d..0ed9d06 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -5257,6 +5257,64 @@ export class DownloadManager extends EventEmitter { return changed; } + private applyPackageExtractFailureStatuses( + completedItems: DownloadItem[], + resolveArchiveItems: (archiveName: string) => DownloadItem[], + failedArchiveErrors: Map, + fallbackReason: string, + previousStatuses: Map, + appliedAt = nowMs() + ): void { + const affectedItemIds = new Set(); + + for (const [archiveName, errorText] of failedArchiveErrors.entries()) { + const reason = compactErrorText(errorText || fallbackReason || "Entpacken fehlgeschlagen"); + for (const entry of resolveArchiveItems(archiveName)) { + if (entry.status !== "completed" || isExtractedLabel(entry.fullStatus)) { + continue; + } + entry.fullStatus = `Entpack-Fehler: ${reason}`; + entry.updatedAt = appliedAt; + affectedItemIds.add(entry.id); + } + } + + let appliedSpecificFailure = affectedItemIds.size > 0; + for (const entry of completedItems) { + if (entry.status !== "completed" || isExtractedLabel(entry.fullStatus)) { + continue; + } + if (affectedItemIds.has(entry.id)) { + continue; + } + + const currentStatus = String(entry.fullStatus || "").trim(); + if (currentStatus === "Entpacken - Error") { + entry.fullStatus = `Entpack-Fehler: ${fallbackReason}`; + entry.updatedAt = appliedAt; + appliedSpecificFailure = true; + continue; + } + + if (/^Entpacken\b/i.test(currentStatus) || /^Passwort\b/i.test(currentStatus) || /^Finalisieren\b/i.test(currentStatus)) { + const previousStatus = String(previousStatuses.get(entry.id) || "").trim(); + entry.fullStatus = previousStatus || `Fertig (${humanSize(entry.downloadedBytes)})`; + entry.updatedAt = appliedAt; + } + } + + if (appliedSpecificFailure) { + return; + } + + for (const entry of completedItems) { + if (entry.status === "completed" && !isExtractedLabel(entry.fullStatus)) { + entry.fullStatus = `Entpack-Fehler: ${fallbackReason}`; + entry.updatedAt = appliedAt; + } + } + } + private async waitForCompletedArchiveFilesToSettle( pkg: PackageEntry, items: DownloadItem[], @@ -9645,6 +9703,7 @@ export class DownloadManager extends EventEmitter { pkg.status = "extracting"; this.emitState(); const extractionStartMs = nowMs(); + const preExtractStatuses = new Map(); const resolveArchiveItems = (archiveName: string): DownloadItem[] => resolveArchiveItemsFromList(archiveName, completedItems); @@ -9663,6 +9722,7 @@ export class DownloadManager extends EventEmitter { // Mark all items as pending before extraction starts for (const entry of completedItems) { if (!isExtractedLabel(entry.fullStatus)) { + preExtractStatuses.set(entry.id, String(entry.fullStatus || "").trim()); entry.fullStatus = "Entpacken - Ausstehend"; entry.updatedAt = nowMs(); } @@ -9698,6 +9758,7 @@ export class DownloadManager extends EventEmitter { try { // Track archives for parallel extraction progress const autoRecoveredArchives = new Set(); + const fullFailedArchiveErrors = new Map(); const fullResolvedItems = new Map(); const fullStartTimes = new Map(); let fullLastProgressCurrent: number | null = null; @@ -9737,7 +9798,13 @@ export class DownloadManager extends EventEmitter { const changed = this.autoRecoverArchiveCrcFailure(pkg, completedItems, failure, "full"); if (changed > 0) { autoRecoveredArchives.add(failure.archiveName); + fullFailedArchiveErrors.delete(failure.archiveName); + return; } + fullFailedArchiveErrors.set( + failure.archiveName, + failure.errorText || failure.jvmFailureReason || "Entpacken fehlgeschlagen" + ); }, onProgress: (progress) => { if (progress.phase === "preparing") { @@ -9872,13 +9939,14 @@ export class DownloadManager extends EventEmitter { if (result.failed > 0) { const reason = compactErrorText(result.lastError || "Entpacken fehlgeschlagen"); const failAt = nowMs(); - for (const entry of completedItems) { - // Preserve per-archive "Entpackt - Done (X.Xs)" labels for successfully extracted archives - if (entry.status === "completed" && !isExtractedLabel(entry.fullStatus)) { - entry.fullStatus = `Entpack-Fehler: ${reason}`; - entry.updatedAt = failAt; - } - } + this.applyPackageExtractFailureStatuses( + completedItems, + resolveArchiveItems, + fullFailedArchiveErrors, + reason, + preExtractStatuses, + failAt + ); pkg.status = "failed"; } else { const hasExtractedOutput = await this.directoryHasAnyFiles(pkg.extractDir); diff --git a/tests/download-manager.test.ts b/tests/download-manager.test.ts index 4a322e9..c9835cb 100644 --- a/tests/download-manager.test.ts +++ b/tests/download-manager.test.ts @@ -3840,6 +3840,70 @@ describe("download manager", () => { expect((manager as any).session.items[itemId].fullStatus).toBe("Entpack-Fehler: Checksum error in encrypted file"); }); + it("applies final extract errors only to the affected full-extract archive items", () => { + const createdAt = Date.now() - 10_000; + const completedItems = [ + { + id: "full-fail-item-1", + status: "completed", + fileName: "show.s01e01.part1.rar", + downloadedBytes: 100 * 1024 * 1024, + fullStatus: "Fertig (100 MB)", + updatedAt: createdAt + }, + { + id: "full-fail-item-2", + status: "completed", + fileName: "show.s01e01.part2.rar", + downloadedBytes: 100 * 1024 * 1024, + fullStatus: "Fertig (100 MB)", + updatedAt: createdAt + }, + { + id: "full-fail-item-3", + status: "completed", + fileName: "show.s01e02.part1.rar", + downloadedBytes: 200 * 1024 * 1024, + fullStatus: "Fertig (200 MB)", + updatedAt: createdAt + }, + { + id: "full-fail-item-4", + status: "completed", + fileName: "show.s01e02.part2.rar", + downloadedBytes: 200 * 1024 * 1024, + fullStatus: "Fertig (200 MB)", + updatedAt: createdAt + } + ] as any[]; + const previousStatuses = new Map(completedItems.map((item: any) => [item.id, item.fullStatus])); + + for (const item of completedItems) { + item.fullStatus = "Entpacken - Ausstehend"; + } + completedItems[0].fullStatus = "Entpacken - Error"; + completedItems[1].fullStatus = "Entpacken - Error"; + const resolveArchiveItems = (archiveName: string) => { + const base = archiveName.replace(/\.part0*1\.rar$/i, ""); + return completedItems.filter((item: any) => String(item.fileName || "").toLowerCase().startsWith(`${base}.part`)); + }; + + (DownloadManager.prototype as any).applyPackageExtractFailureStatuses.call( + {}, + completedItems, + resolveArchiveItems, + new Map([["show.s01e01.part1.rar", "Checksum error in the encrypted file"]]), + "Checksum error in the encrypted file", + previousStatuses, + createdAt + 5_000 + ); + + expect(completedItems[0].fullStatus).toBe("Entpack-Fehler: Checksum error in the encrypted file"); + expect(completedItems[1].fullStatus).toBe("Entpack-Fehler: Checksum error in the encrypted file"); + expect(completedItems[2].fullStatus).toBe("Fertig (200 MB)"); + expect(completedItems[3].fullStatus).toBe("Fertig (200 MB)"); + }); + it("detects start conflicts when extract output already exists", async () => { const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-")); tempDirs.push(root);