From 674cf101dac779b41fbcd3320b54a97973d54d3b Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Sun, 1 Mar 2026 20:57:56 +0100 Subject: [PATCH] Fix extraction status cross-contamination with filename pattern matching, release v1.4.70 Previous fix used pathKey-based maps which failed due to path resolution mismatches on Windows. New approach matches items to archives using filename regex patterns directly (e.g. prefix.part\d+.rar), which is robust regardless of path casing/resolution. Also marks items as "Entpackt" immediately when their archive finishes instead of waiting for all archives to complete, so completed episodes show correct status while later episodes are still extracting. Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- src/main/download-manager.ts | 107 +++++++++++++++++++++++++---------- 2 files changed, 77 insertions(+), 32 deletions(-) diff --git a/package.json b/package.json index dc03923..3f47acf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.4.69", + "version": "1.4.70", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index e735fb2..ba63d78 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -4415,7 +4415,6 @@ export class DownloadManager extends EventEmitter { // Build set of item targetPaths belonging to ready archives const hybridItemPaths = new Set(); - const archiveToItems = new Map(); let dirFiles: string[] | undefined; try { dirFiles = fs.readdirSync(pkg.outputDir, { withFileTypes: true }) @@ -4424,22 +4423,35 @@ export class DownloadManager extends EventEmitter { } catch { /* ignore */ } for (const archiveKey of readyArchives) { const parts = collectArchiveCleanupTargets(archiveKey, dirFiles); - const partKeys = new Set(); for (const part of parts) { hybridItemPaths.add(pathKey(part)); - partKeys.add(pathKey(part)); } hybridItemPaths.add(pathKey(archiveKey)); - partKeys.add(pathKey(archiveKey)); - const matched = completedItems.filter((item) => item.targetPath && partKeys.has(pathKey(item.targetPath))); - if (matched.length > 0) { - archiveToItems.set(path.basename(archiveKey).toLowerCase(), matched); - } } const hybridItems = completedItems.filter((item) => item.targetPath && hybridItemPaths.has(pathKey(item.targetPath)) ); + // Resolve items belonging to a specific archive entry point by filename pattern matching. + // This avoids pathKey mismatches by comparing basenames directly. + const resolveArchiveItems = (archiveName: string): DownloadItem[] => { + const entryLower = archiveName.toLowerCase(); + const multipartMatch = entryLower.match(/^(.*)\.part0*1\.rar$/); + if (multipartMatch) { + const prefix = multipartMatch[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const pattern = new RegExp(`^${prefix}\\.part\\d+\\.rar$`, "i"); + return hybridItems.filter((item) => { + const name = path.basename(item.targetPath || item.fileName || ""); + return pattern.test(name); + }); + } + // Single-file archive: match only that exact file + return hybridItems.filter((item) => { + const name = path.basename(item.targetPath || item.fileName || "").toLowerCase(); + return name === entryLower; + }); + }; + let currentArchiveItems: DownloadItem[] = hybridItems; const updateExtractingStatus = (text: string): void => { const normalized = String(text || ""); @@ -4462,6 +4474,7 @@ export class DownloadManager extends EventEmitter { let hybridLastStatusText = ""; let hybridLastEmitAt = 0; + let lastHybridArchiveName = ""; const emitHybridStatus = (text: string, force = false): void => { updateExtractingStatus(text); const now = nowMs(); @@ -4491,9 +4504,20 @@ export class DownloadManager extends EventEmitter { if (progress.phase === "done") { return; } - // Narrow status updates to only items belonging to the current archive - if (progress.archiveName) { - currentArchiveItems = archiveToItems.get(progress.archiveName.toLowerCase()) || hybridItems; + // When a new archive starts, mark the previous archive's items as "Entpackt" + if (progress.archiveName && progress.archiveName !== lastHybridArchiveName) { + if (lastHybridArchiveName && currentArchiveItems !== hybridItems) { + const doneAt = nowMs(); + for (const entry of currentArchiveItems) { + if (!isExtractedLabel(entry.fullStatus)) { + entry.fullStatus = "Entpackt"; + entry.updatedAt = doneAt; + } + } + } + lastHybridArchiveName = progress.archiveName; + const resolved = resolveArchiveItems(progress.archiveName); + currentArchiveItems = resolved.length > 0 ? resolved : hybridItems; } const archive = progress.archiveName ? ` ยท ${progress.archiveName}` : ""; const elapsed = progress.elapsedMs && progress.elapsedMs >= 1000 @@ -4577,24 +4601,33 @@ export class DownloadManager extends EventEmitter { pkg.status = "extracting"; this.emitState(); - // Build map: archive basename -> items belonging to that archive set - const archiveToItems = new Map(); - let dirFiles: string[] | undefined; - try { - dirFiles = fs.readdirSync(pkg.outputDir, { withFileTypes: true }) - .filter((entry) => entry.isFile()) - .map((entry) => entry.name); - } catch { /* ignore */ } - const candidates = findArchiveCandidates(pkg.outputDir); - for (const candidate of candidates) { - const parts = collectArchiveCleanupTargets(candidate, dirFiles); - const partKeys = new Set(parts.map((p) => pathKey(p))); - partKeys.add(pathKey(candidate)); - const matched = completedItems.filter((item) => item.targetPath && partKeys.has(pathKey(item.targetPath))); - if (matched.length > 0) { - archiveToItems.set(path.basename(candidate).toLowerCase(), matched); + // Resolve items belonging to a specific archive entry point by filename pattern matching + const resolveArchiveItems = (archiveName: string): DownloadItem[] => { + const entryLower = archiveName.toLowerCase(); + const multipartMatch = entryLower.match(/^(.*)\.part0*1\.rar$/); + if (multipartMatch) { + const prefix = multipartMatch[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const pattern = new RegExp(`^${prefix}\\.part\\d+\\.rar$`, "i"); + return completedItems.filter((item) => { + const name = path.basename(item.targetPath || item.fileName || ""); + return pattern.test(name); + }); } - } + // Single-file archive or non-multipart RAR: match based on archive stem + const rarMatch = entryLower.match(/^(.*)\.rar$/); + if (rarMatch) { + const stem = rarMatch[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const pattern = new RegExp(`^${stem}\\.r(ar|\\d{2,3})$`, "i"); + return completedItems.filter((item) => { + const name = path.basename(item.targetPath || item.fileName || ""); + return pattern.test(name); + }); + } + return completedItems.filter((item) => { + const name = path.basename(item.targetPath || item.fileName || "").toLowerCase(); + return name === entryLower; + }); + }; let currentArchiveItems: DownloadItem[] = completedItems; const updateExtractingStatus = (text: string): void => { @@ -4618,6 +4651,7 @@ export class DownloadManager extends EventEmitter { let lastExtractStatusText = ""; let lastExtractEmitAt = 0; + let lastExtractArchiveName = ""; const emitExtractStatus = (text: string, force = false): void => { updateExtractingStatus(text); const now = nowMs(); @@ -4668,9 +4702,20 @@ export class DownloadManager extends EventEmitter { signal: extractAbortController.signal, packageId, onProgress: (progress) => { - // Narrow status updates to only items belonging to the current archive - if (progress.archiveName) { - currentArchiveItems = archiveToItems.get(progress.archiveName.toLowerCase()) || completedItems; + // When a new archive starts, mark the previous archive's items as "Entpackt" + if (progress.archiveName && progress.archiveName !== lastExtractArchiveName) { + if (lastExtractArchiveName && currentArchiveItems !== completedItems) { + const doneAt = nowMs(); + for (const entry of currentArchiveItems) { + if (!isExtractedLabel(entry.fullStatus)) { + entry.fullStatus = "Entpackt"; + entry.updatedAt = doneAt; + } + } + } + lastExtractArchiveName = progress.archiveName; + const resolved = resolveArchiveItems(progress.archiveName); + currentArchiveItems = resolved.length > 0 ? resolved : completedItems; } const label = progress.phase === "done" ? "Entpacken 100%"