From 28113f57f3cf6e7dbad2b2295170c865dfdbb5ed Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Sun, 8 Mar 2026 02:28:51 +0100 Subject: [PATCH] Release v1.7.44 --- package-lock.json | 4 +- package.json | 2 +- src/main/extractor.ts | 97 +++++++++++++++++++++++++++++++++++------ tests/extractor.test.ts | 30 +++++++++++++ 4 files changed, 117 insertions(+), 16 deletions(-) diff --git a/package-lock.json b/package-lock.json index 238067b..15cc1f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "real-debrid-downloader", - "version": "1.7.43", + "version": "1.7.44", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "real-debrid-downloader", - "version": "1.7.43", + "version": "1.7.44", "license": "MIT", "dependencies": { "adm-zip": "^0.5.16", diff --git a/package.json b/package.json index 837e6c4..e121cff 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.7.43", + "version": "1.7.44", "description": "Desktop downloader", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/extractor.ts b/src/main/extractor.ts index e36bf30..5af1bcf 100644 --- a/src/main/extractor.ts +++ b/src/main/extractor.ts @@ -679,7 +679,7 @@ function prioritizePassword(passwords: string[], successful: string): string[] { return next; } -function nativeExtractorCandidates(): string[] { +function nativeExtractorCandidates(archivePath = ""): string[] { const programFiles = process.env.ProgramFiles || "C:\\Program Files"; const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)"; const localAppData = process.env.LOCALAPPDATA || ""; @@ -694,11 +694,14 @@ function nativeExtractorCandidates(): string[] { } const winRarInstalled = [ + path.join(programFiles, "WinRAR", "WinRAR.exe"), + path.join(programFilesX86, "WinRAR", "WinRAR.exe"), path.join(programFiles, "WinRAR", "UnRAR.exe"), path.join(programFilesX86, "WinRAR", "UnRAR.exe") ]; if (localAppData) { + winRarInstalled.push(path.join(localAppData, "Programs", "WinRAR", "WinRAR.exe")); winRarInstalled.push(path.join(localAppData, "Programs", "WinRAR", "UnRAR.exe")); } @@ -711,6 +714,8 @@ function nativeExtractorCandidates(): string[] { "7za.exe", "7za", ...winRarInstalled, + "WinRAR.exe", + "winrar", "UnRAR.exe", "unrar" ] @@ -721,10 +726,12 @@ function nativeExtractorCandidates(): string[] { "7za.exe", "7za", ...winRarInstalled, + "WinRAR.exe", + "winrar", "UnRAR.exe", "unrar" ]; - return Array.from(new Set(ordered.filter(Boolean))); + return orderExtractorCandidatesForArchive(ordered, archivePath, resolvedExtractorCommand || ""); } function isAbsoluteCommand(command: string): boolean { @@ -1786,7 +1793,7 @@ export function buildExternalExtractArgs( return ["x", "-y", overwrite, pass, archivePath, `-o${targetDir}`]; } -async function resolveExtractorCommandInternal(): Promise { +async function resolveExtractorCommandInternal(archivePath = ""): Promise { if (resolvedExtractorCommand) { return resolvedExtractorCommand; } @@ -1799,7 +1806,7 @@ async function resolveExtractorCommandInternal(): Promise { resolveFailureAt = 0; } - const candidates = nativeExtractorCandidates(); + const candidates = nativeExtractorCandidates(archivePath); for (const command of candidates) { if (isAbsoluteCommand(command) && !fs.existsSync(command)) { continue; @@ -1821,15 +1828,15 @@ async function resolveExtractorCommandInternal(): Promise { throw new Error(resolveFailureReason); } -async function resolveExtractorCommand(): Promise { - if (resolvedExtractorCommand) { +async function resolveExtractorCommand(archivePath = ""): Promise { + if (resolvedExtractorCommand && cachedExtractorFitsArchive(resolvedExtractorCommand, archivePath)) { return resolvedExtractorCommand; } if (resolveExtractorCommandInFlight) { return resolveExtractorCommandInFlight; } - const pending = resolveExtractorCommandInternal(); + const pending = resolveExtractorCommandInternal(archivePath); resolveExtractorCommandInFlight = pending; try { return await pending; @@ -1850,8 +1857,71 @@ function isUnrarCommand(command: string): boolean { return lower.includes("unrar") || lower.includes("winrar"); } -async function findAlternativeExtractor(currentCommand: string): Promise { - const candidates = nativeExtractorCandidates(); +type ExtractorCommandKind = "rar_native" | "seven_zip" | "other"; + +function extractorCommandKind(command: string): ExtractorCommandKind { + if (isUnrarCommand(command)) { + return "rar_native"; + } + if (is7zCommand(command)) { + return "seven_zip"; + } + return "other"; +} + +function isRarArchivePath(filePath: string): boolean { + return /\.(?:rar|r\d{2,3})$/i.test(String(filePath || "")); +} + +function cachedExtractorFitsArchive(command: string, archivePath: string): boolean { + if (!archivePath) { + return true; + } + const kind = extractorCommandKind(command); + if (isRarArchivePath(archivePath)) { + return kind === "rar_native"; + } + return kind === "seven_zip"; +} + +export function orderExtractorCandidatesForArchive( + candidates: string[], + archivePath: string, + preferredCommand = "" +): string[] { + const unique = Array.from(new Set(candidates.filter(Boolean))); + const preferRarNative = isRarArchivePath(archivePath); + const rank = (command: string): number => { + const kind = extractorCommandKind(command); + if (preferRarNative) { + if (kind === "rar_native") return 0; + if (kind === "seven_zip") return 1; + return 2; + } + if (kind === "seven_zip") return 0; + if (kind === "rar_native") return 1; + return 2; + }; + + return unique + .map((command, index) => ({ command, index })) + .sort((left, right) => { + const rankDiff = rank(left.command) - rank(right.command); + if (rankDiff !== 0) { + return rankDiff; + } + const leftPreferred = preferredCommand && left.command === preferredCommand; + const rightPreferred = preferredCommand && right.command === preferredCommand; + if (leftPreferred !== rightPreferred) { + return leftPreferred ? -1 : 1; + } + return left.index - right.index; + }) + .map((entry) => entry.command); +} + +async function findAlternativeExtractor(currentCommand: string, archivePath = ""): Promise { + const candidates = nativeExtractorCandidates(archivePath); const currentIs7z = is7zCommand(currentCommand); for (const candidate of candidates) { if (candidate === currentCommand) continue; @@ -1971,7 +2041,7 @@ async function runExternalExtract( subst = createSubstMapping(targetDir); const effectiveTargetDir = subst ? `${subst.drive}:\\` : targetDir; - const command = await resolveExtractorCommand(); + const command = await resolveExtractorCommand(archivePath); const legacyStartedAt = Date.now(); let password: string; let usedCommand = command; @@ -1999,7 +2069,7 @@ async function runExternalExtract( const errText = String((primaryError as Error)?.message || primaryError || ""); const isPasswordOrCorrupt = /wrong.password|checksum error|corrupt/i.test(errText); if (isRar && isPasswordOrCorrupt && !signal?.aborted) { - const alt = await findAlternativeExtractor(command); + const alt = await findAlternativeExtractor(command, archivePath); if (alt) { const altName = path.basename(alt).replace(/\.exe$/i, ""); onLog?.("INFO", `Legacy-Fallback: primary=${path.basename(command)}, alternative=${altName}, archive=${archiveName}`); @@ -2121,10 +2191,11 @@ async function runExternalExtractInner( ): Promise { const passwords = passwordCandidates; let lastError = ""; + const extractorName = path.basename(command).replace(/\.exe$/i, "") || command; const quotedPasswords = passwords.map((p) => p === "" ? '""' : `"${p}"`); - onLog?.("INFO", `Legacy-Extractor Start: archive=${path.basename(archivePath)}, passwordCount=${passwords.length}, forceFlatMode=${forceFlatMode}, targetDir=${targetDir}`); - logger.info(`Legacy-Extractor: ${path.basename(archivePath)}, ${passwords.length} Passwörter: [${quotedPasswords.join(", ")}]${forceFlatMode ? " (flat-mode cached)" : ""}`); + onLog?.("INFO", `Legacy-Extractor Start: archive=${path.basename(archivePath)}, extractor=${extractorName}, passwordCount=${passwords.length}, forceFlatMode=${forceFlatMode}, targetDir=${targetDir}`); + logger.info(`Legacy-Extractor (${extractorName}): ${path.basename(archivePath)}, ${passwords.length} Passwörter: [${quotedPasswords.join(", ")}]${forceFlatMode ? " (flat-mode cached)" : ""}`); let announcedStart = false; let bestPercent = 0; diff --git a/tests/extractor.test.ts b/tests/extractor.test.ts index 872cf7e..889c315 100644 --- a/tests/extractor.test.ts +++ b/tests/extractor.test.ts @@ -12,6 +12,7 @@ import { detectArchiveSignature, classifyExtractionError, findArchiveCandidates, + orderExtractorCandidatesForArchive, resolveExtractorBackendMode, } from "../src/main/extractor"; @@ -1171,4 +1172,33 @@ describe("extractor", () => { expect(resolveExtractorBackendMode("auto", false)).toBe("auto"); }); }); + + describe("orderExtractorCandidatesForArchive", () => { + it("prefers WinRAR/UnRAR over 7-Zip for rar archives", () => { + const ordered = orderExtractorCandidatesForArchive( + ["7z.exe", "UnRAR.exe", "WinRAR.exe"], + "C:\\Downloads\\archive.part01.rar" + ); + expect(ordered.slice(0, 2)).toEqual(["UnRAR.exe", "WinRAR.exe"]); + expect(ordered[2]).toBe("7z.exe"); + }); + + it("keeps 7-Zip first for non-rar archives", () => { + const ordered = orderExtractorCandidatesForArchive( + ["UnRAR.exe", "7z.exe", "WinRAR.exe"], + "C:\\Downloads\\archive.zip" + ); + expect(ordered[0]).toBe("7z.exe"); + }); + + it("prefers the remembered command within the matching archive class", () => { + const ordered = orderExtractorCandidatesForArchive( + ["UnRAR.exe", "WinRAR.exe", "7z.exe"], + "C:\\Downloads\\archive.part01.rar", + "WinRAR.exe" + ); + expect(ordered[0]).toBe("WinRAR.exe"); + expect(ordered[1]).toBe("UnRAR.exe"); + }); + }); });