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 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-03-01 20:57:56 +01:00
parent 4371e53b86
commit 674cf101da
2 changed files with 77 additions and 32 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.4.69", "version": "1.4.70",
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
"main": "build/main/main/main.js", "main": "build/main/main/main.js",
"author": "Sucukdeluxe", "author": "Sucukdeluxe",

View File

@ -4415,7 +4415,6 @@ export class DownloadManager extends EventEmitter {
// Build set of item targetPaths belonging to ready archives // Build set of item targetPaths belonging to ready archives
const hybridItemPaths = new Set<string>(); const hybridItemPaths = new Set<string>();
const archiveToItems = new Map<string, DownloadItem[]>();
let dirFiles: string[] | undefined; let dirFiles: string[] | undefined;
try { try {
dirFiles = fs.readdirSync(pkg.outputDir, { withFileTypes: true }) dirFiles = fs.readdirSync(pkg.outputDir, { withFileTypes: true })
@ -4424,22 +4423,35 @@ export class DownloadManager extends EventEmitter {
} catch { /* ignore */ } } catch { /* ignore */ }
for (const archiveKey of readyArchives) { for (const archiveKey of readyArchives) {
const parts = collectArchiveCleanupTargets(archiveKey, dirFiles); const parts = collectArchiveCleanupTargets(archiveKey, dirFiles);
const partKeys = new Set<string>();
for (const part of parts) { for (const part of parts) {
hybridItemPaths.add(pathKey(part)); hybridItemPaths.add(pathKey(part));
partKeys.add(pathKey(part));
} }
hybridItemPaths.add(pathKey(archiveKey)); 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) => const hybridItems = completedItems.filter((item) =>
item.targetPath && hybridItemPaths.has(pathKey(item.targetPath)) 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; let currentArchiveItems: DownloadItem[] = hybridItems;
const updateExtractingStatus = (text: string): void => { const updateExtractingStatus = (text: string): void => {
const normalized = String(text || ""); const normalized = String(text || "");
@ -4462,6 +4474,7 @@ export class DownloadManager extends EventEmitter {
let hybridLastStatusText = ""; let hybridLastStatusText = "";
let hybridLastEmitAt = 0; let hybridLastEmitAt = 0;
let lastHybridArchiveName = "";
const emitHybridStatus = (text: string, force = false): void => { const emitHybridStatus = (text: string, force = false): void => {
updateExtractingStatus(text); updateExtractingStatus(text);
const now = nowMs(); const now = nowMs();
@ -4491,9 +4504,20 @@ export class DownloadManager extends EventEmitter {
if (progress.phase === "done") { if (progress.phase === "done") {
return; return;
} }
// Narrow status updates to only items belonging to the current archive // When a new archive starts, mark the previous archive's items as "Entpackt"
if (progress.archiveName) { if (progress.archiveName && progress.archiveName !== lastHybridArchiveName) {
currentArchiveItems = archiveToItems.get(progress.archiveName.toLowerCase()) || hybridItems; 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 archive = progress.archiveName ? ` · ${progress.archiveName}` : "";
const elapsed = progress.elapsedMs && progress.elapsedMs >= 1000 const elapsed = progress.elapsedMs && progress.elapsedMs >= 1000
@ -4577,24 +4601,33 @@ export class DownloadManager extends EventEmitter {
pkg.status = "extracting"; pkg.status = "extracting";
this.emitState(); this.emitState();
// Build map: archive basename -> items belonging to that archive set // Resolve items belonging to a specific archive entry point by filename pattern matching
const archiveToItems = new Map<string, DownloadItem[]>(); const resolveArchiveItems = (archiveName: string): DownloadItem[] => {
let dirFiles: string[] | undefined; const entryLower = archiveName.toLowerCase();
try { const multipartMatch = entryLower.match(/^(.*)\.part0*1\.rar$/);
dirFiles = fs.readdirSync(pkg.outputDir, { withFileTypes: true }) if (multipartMatch) {
.filter((entry) => entry.isFile()) const prefix = multipartMatch[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
.map((entry) => entry.name); const pattern = new RegExp(`^${prefix}\\.part\\d+\\.rar$`, "i");
} catch { /* ignore */ } return completedItems.filter((item) => {
const candidates = findArchiveCandidates(pkg.outputDir); const name = path.basename(item.targetPath || item.fileName || "");
for (const candidate of candidates) { return pattern.test(name);
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);
} }
// 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; let currentArchiveItems: DownloadItem[] = completedItems;
const updateExtractingStatus = (text: string): void => { const updateExtractingStatus = (text: string): void => {
@ -4618,6 +4651,7 @@ export class DownloadManager extends EventEmitter {
let lastExtractStatusText = ""; let lastExtractStatusText = "";
let lastExtractEmitAt = 0; let lastExtractEmitAt = 0;
let lastExtractArchiveName = "";
const emitExtractStatus = (text: string, force = false): void => { const emitExtractStatus = (text: string, force = false): void => {
updateExtractingStatus(text); updateExtractingStatus(text);
const now = nowMs(); const now = nowMs();
@ -4668,9 +4702,20 @@ export class DownloadManager extends EventEmitter {
signal: extractAbortController.signal, signal: extractAbortController.signal,
packageId, packageId,
onProgress: (progress) => { onProgress: (progress) => {
// Narrow status updates to only items belonging to the current archive // When a new archive starts, mark the previous archive's items as "Entpackt"
if (progress.archiveName) { if (progress.archiveName && progress.archiveName !== lastExtractArchiveName) {
currentArchiveItems = archiveToItems.get(progress.archiveName.toLowerCase()) || completedItems; 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" const label = progress.phase === "done"
? "Entpacken 100%" ? "Entpacken 100%"