From 55b00bf8842fa02eda8dd9d63e62418206391616 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Wed, 4 Mar 2026 20:18:47 +0100 Subject: [PATCH] Release v1.6.26 Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- src/main/download-manager.ts | 90 ++++++++++++++++++++++++++++++++++-- src/main/extractor.ts | 10 ++-- 3 files changed, 91 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 1007187..74e5905 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.6.25", + "version": "1.6.26", "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 093a252..17d5309 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -751,6 +751,36 @@ function resolveArchiveItemsFromList(archiveName: string, items: DownloadItem[]) return pattern.test(name); }); } + // Split ZIP (e.g., movie.zip.001, movie.zip.002) + const zipSplitMatch = entryLower.match(/^(.*)\.zip\.001$/); + if (zipSplitMatch) { + const stem = zipSplitMatch[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const pattern = new RegExp(`^${stem}\\.zip(\\.\\d+)?$`, "i"); + return items.filter((item) => { + const name = path.basename(item.targetPath || item.fileName || ""); + return pattern.test(name); + }); + } + // Split 7z (e.g., movie.7z.001, movie.7z.002) + const sevenSplitMatch = entryLower.match(/^(.*)\.7z\.001$/); + if (sevenSplitMatch) { + const stem = sevenSplitMatch[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const pattern = new RegExp(`^${stem}\\.7z(\\.\\d+)?$`, "i"); + return items.filter((item) => { + const name = path.basename(item.targetPath || item.fileName || ""); + return pattern.test(name); + }); + } + // Generic .NNN splits (e.g., movie.001, movie.002) + const genericSplitMatch = entryLower.match(/^(.*)\.001$/); + if (genericSplitMatch && !/\.(zip|7z)\.001$/.test(entryLower)) { + const stem = genericSplitMatch[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + const pattern = new RegExp(`^${stem}\\.\\d{3}$`, "i"); + return items.filter((item) => { + const name = path.basename(item.targetPath || item.fileName || ""); + return pattern.test(name); + }); + } return items.filter((item) => { const name = path.basename(item.targetPath || item.fileName || "").toLowerCase(); return name === entryLower; @@ -1680,6 +1710,9 @@ export class DownloadManager extends EventEmitter { item.lastError = "Datei nicht gefunden auf Rapidgator"; item.onlineStatus = "offline"; item.updatedAt = nowMs(); + if (this.runItemIds.has(itemId)) { + this.recordRunOutcome(itemId, "failed"); + } // Refresh package status since item was set to failed const pkg = this.session.packages[item.packageId]; if (pkg) this.refreshPackageStatus(pkg); @@ -2460,7 +2493,10 @@ export class DownloadManager extends EventEmitter { if (!item) { continue; } - this.recordRunOutcome(itemId, "cancelled"); + // Only overwrite outcome for non-completed items to preserve correct summary stats + if (item.status !== "completed") { + this.recordRunOutcome(itemId, "cancelled"); + } const active = this.activeTasks.get(itemId); if (active) { active.abortReason = "cancel"; @@ -2687,6 +2723,25 @@ export class DownloadManager extends EventEmitter { const pkg = this.session.packages[pkgId]; if (pkg) this.refreshPackageStatus(pkg); } + // Trigger extraction if all items are now in a terminal state and some completed + if (this.settings.autoExtract) { + for (const pkgId of affectedPackageIds) { + const pkg = this.session.packages[pkgId]; + if (!pkg || pkg.cancelled || this.packagePostProcessTasks.has(pkgId)) continue; + const pkgItems = pkg.itemIds.map((id) => this.session.items[id]).filter(Boolean) as DownloadItem[]; + const hasPending = pkgItems.some((i) => i.status !== "completed" && i.status !== "failed" && i.status !== "cancelled"); + const hasUnextracted = pkgItems.some((i) => i.status === "completed" && !isExtractedLabel(i.fullStatus || "")); + if (!hasPending && hasUnextracted) { + for (const it of pkgItems) { + if (it.status === "completed" && !isExtractedLabel(it.fullStatus || "")) { + it.fullStatus = "Entpacken - Ausstehend"; + it.updatedAt = nowMs(); + } + } + void this.runPackagePostProcessing(pkgId).catch((err) => logger.warn(`Post-processing nach Skip: ${compactErrorText(err)}`)); + } + } + } this.persistSoon(); this.emitState(); } @@ -3196,6 +3251,7 @@ export class DownloadManager extends EventEmitter { item.fullStatus = "Wartet"; item.lastError = ""; item.speedBps = 0; + item.updatedAt = nowMs(); continue; } if (item.status === "extracting" || item.status === "integrity_check") { @@ -3204,6 +3260,7 @@ export class DownloadManager extends EventEmitter { item.status = "completed"; item.fullStatus = `Fertig (${humanSize(item.downloadedBytes)})`; item.speedBps = 0; + item.updatedAt = nowMs(); } else if (item.status === "downloading" || item.status === "validating" || item.status === "paused" @@ -3211,6 +3268,7 @@ export class DownloadManager extends EventEmitter { item.status = "queued"; item.fullStatus = "Wartet"; item.speedBps = 0; + item.updatedAt = nowMs(); } // Clear stale transient status texts from previous session if (item.status === "queued") { @@ -3292,6 +3350,7 @@ export class DownloadManager extends EventEmitter { if (!pkg) { continue; } + const completedItemIds: string[] = []; pkg.itemIds = pkg.itemIds.filter((itemId) => { const item = this.session.items[itemId]; if (!item) { @@ -3302,14 +3361,18 @@ export class DownloadManager extends EventEmitter { if (this.settings.autoExtract && !isExtractedLabel(item.fullStatus || "")) { return true; } - delete this.session.items[itemId]; - this.itemCount = Math.max(0, this.itemCount - 1); + completedItemIds.push(itemId); return false; } return true; }); if (pkg.itemIds.length === 0) { - this.removePackageFromSession(pkgId, []); + this.removePackageFromSession(pkgId, completedItemIds); + } else { + for (const itemId of completedItemIds) { + delete this.session.items[itemId]; + this.itemCount = Math.max(0, this.itemCount - 1); + } } } } @@ -3349,7 +3412,7 @@ export class DownloadManager extends EventEmitter { } else if (policy === "package_done" || policy === "on_start") { const allCompleted = pkg.itemIds.every((id) => { const item = this.session.items[id]; - return !item || item.status === "completed"; + return !item || item.status === "completed" || item.status === "failed" || item.status === "cancelled"; }); if (!allCompleted) continue; if (this.settings.autoExtract) { @@ -4624,6 +4687,7 @@ export class DownloadManager extends EventEmitter { if (item.attempts < maxAttempts) { item.status = "integrity_check"; item.progressPercent = 0; + this.dropItemContribution(item.id); item.downloadedBytes = 0; item.totalBytes = unrestricted.fileSize; this.emitState(); @@ -4992,6 +5056,15 @@ export class DownloadManager extends EventEmitter { } catch { // file does not exist } + // Guard against pre-allocated sparse files from a crashed session: + // if file size exceeds persisted downloadedBytes by >1MB, the file was + // likely pre-allocated but only partially written before a hard crash. + if (existingBytes > 0 && item.downloadedBytes > 0 && existingBytes > item.downloadedBytes + 1048576) { + try { + await fs.promises.truncate(effectiveTargetPath, item.downloadedBytes); + existingBytes = item.downloadedBytes; + } catch { /* best-effort */ } + } const headers: Record = {}; if (existingBytes > 0) { headers.Range = `bytes=${existingBytes}-`; @@ -6071,6 +6144,12 @@ export class DownloadManager extends EventEmitter { const escaped = stem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); return new RegExp(`^${escaped}\\.7z(\\.\\d+)?$`, "i").test(fileName); } + // Generic .NNN splits (e.g., movie.001, movie.002) + if (/\.001$/i.test(entryPointName) && !/\.(zip|7z)\.001$/i.test(entryPointName)) { + const stem = entryPointName.replace(/\.001$/i, "").toLowerCase(); + const escaped = stem.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + return new RegExp(`^${escaped}\\.\\d{3}$`, "i").test(fileName); + } return false; } @@ -6755,6 +6834,7 @@ export class DownloadManager extends EventEmitter { delete this.session.items[itemId]; this.itemCount = Math.max(0, this.itemCount - 1); this.retryAfterByItem.delete(itemId); + this.retryStateByItem.delete(itemId); if (pkg.itemIds.length === 0) { this.removePackageFromSession(packageId, []); } diff --git a/src/main/extractor.ts b/src/main/extractor.ts index ba42005..23683f5 100644 --- a/src/main/extractor.ts +++ b/src/main/extractor.ts @@ -209,7 +209,7 @@ function archiveSortKey(filePath: string): string { .replace(/\.zip\.\d{3}$/i, "") .replace(/\.7z\.\d{3}$/i, "") .replace(/\.\d{3}$/i, "") - .replace(/\.tar\.(gz|bz2|xz)$/i, "") + .replace(/\.(?:tar\.(?:gz|bz2|xz)|tgz|tbz2|txz)$/i, "") .replace(/\.rar$/i, "") .replace(/\.zip$/i, "") .replace(/\.7z$/i, "") @@ -230,7 +230,7 @@ function archiveTypeRank(filePath: string): number { if (/\.7z(?:\.\d{3})?$/i.test(fileName)) { return 3; } - if (/\.tar\.(gz|bz2|xz)$/i.test(fileName)) { + if (/\.(?:tar\.(?:gz|bz2|xz)|tgz|tbz2|txz)$/i.test(fileName)) { return 4; } if (/\.\d{3}$/i.test(fileName)) { @@ -281,7 +281,7 @@ export async function findArchiveCandidates(packageDir: string): Promise /\.tar\.(gz|bz2|xz)$/i.test(filePath)); + const tarCompressed = files.filter((filePath) => /\.(?:tar\.(?:gz|bz2|xz)|tgz|tbz2|txz)$/i.test(filePath)); // Generic .001 splits (HJSplit etc.) — exclude already-recognized .zip.001 and .7z.001 const genericSplit = files.filter((filePath) => { const fileName = path.basename(filePath).toLowerCase(); @@ -477,7 +477,7 @@ export function archiveFilenamePasswords(archiveName: string): string[] { .replace(/\.zip\.\d{3}$/i, "") .replace(/\.7z\.\d{3}$/i, "") .replace(/\.\d{3}$/i, "") - .replace(/\.tar\.(gz|bz2|xz)$/i, "") + .replace(/\.(?:tar\.(?:gz|bz2|xz)|tgz|tbz2|txz)$/i, "") .replace(/\.(rar|zip|7z|tar|gz|bz2|xz)$/i, ""); if (!stem) return []; const candidates = [stem]; @@ -1345,7 +1345,7 @@ async function runExternalExtract( // subst only needed for legacy UnRAR/7z (MAX_PATH limit) subst = createSubstMapping(targetDir); - const effectiveTargetDir = subst ? `${subst.drive}:` : targetDir; + const effectiveTargetDir = subst ? `${subst.drive}:\\` : targetDir; const command = await resolveExtractorCommand(); const password = await runExternalExtractInner(