diff --git a/package-lock.json b/package-lock.json index 0f26d38..250a72e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,14 @@ { "name": "real-debrid-downloader", - "version": "1.1.29", + "version": "1.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "real-debrid-downloader", - "version": "1.1.29", + "version": "1.3.0", "license": "MIT", "dependencies": { - "7zip-bin": "^5.2.0", "adm-zip": "^0.5.16", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -2231,6 +2230,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", + "dev": true, "license": "MIT" }, "node_modules/abbrev": { diff --git a/package.json b/package.json index b1fde0d..0465328 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.1.29", + "version": "1.3.0", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "main": "build/main/main/main.js", "author": "Sucukdeluxe", @@ -22,12 +22,11 @@ "adm-zip": "^0.5.16", "react": "^18.3.1", "react-dom": "^18.3.1", - "7zip-bin": "^5.2.0", "uuid": "^11.1.0" }, "devDependencies": { - "@types/node": "^24.0.13", "@types/adm-zip": "^0.5.7", + "@types/node": "^24.0.13", "@types/react": "^18.3.12", "@types/react-dom": "^18.3.1", "@types/uuid": "^10.0.0", @@ -55,9 +54,6 @@ "build/renderer/**/*", "package.json" ], - "asarUnpack": [ - "node_modules/7zip-bin/**/*" - ], "win": { "target": [ "nsis", diff --git a/src/main/extractor.ts b/src/main/extractor.ts index 424bece..ada94f0 100644 --- a/src/main/extractor.ts +++ b/src/main/extractor.ts @@ -2,17 +2,15 @@ import fs from "node:fs"; import path from "node:path"; import { spawn } from "node:child_process"; import AdmZip from "adm-zip"; -import { path7za } from "7zip-bin"; import { CleanupMode, ConflictMode } from "../shared/types"; import { logger } from "./logger"; import { removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup"; const DEFAULT_ARCHIVE_PASSWORDS = ["", "serienfans.org", "serienjunkies.org"]; -const FALLBACK_COMMANDS = ["7z", "C:\\Program Files\\7-Zip\\7z.exe", "C:\\Program Files (x86)\\7-Zip\\7z.exe", "unrar"]; +const NO_EXTRACTOR_MESSAGE = "WinRAR/UnRAR nicht gefunden. Bitte WinRAR installieren."; -let preferredExtractorCommand: string | null = null; -let extractorUnavailable = false; -let extractorUnavailableReason = ""; +let resolvedExtractorCommand: string | null = null; +let resolveFailureReason = ""; export interface ExtractOptions { packageDir: string; @@ -51,12 +49,6 @@ function cleanErrorText(text: string): string { return String(text || "").replace(/\s+/g, " ").trim().slice(0, 240); } -function normalizeBundledExtractorPath(filePath: string): string { - return filePath.includes("app.asar") - ? filePath.replace("app.asar", "app.asar.unpacked") - : filePath; -} - function archivePasswords(): string[] { const custom = String(process.env.RD_ARCHIVE_PASSWORDS || "") .split(/[;,\n]/g) @@ -65,11 +57,26 @@ function archivePasswords(): string[] { return Array.from(new Set([...DEFAULT_ARCHIVE_PASSWORDS, ...custom])); } -function extractorCandidates(): string[] { - const bundled = normalizeBundledExtractorPath(path7za); - const ordered = preferredExtractorCommand - ? [preferredExtractorCommand, bundled, ...FALLBACK_COMMANDS] - : [bundled, ...FALLBACK_COMMANDS]; +function winRarCandidates(): string[] { + const programFiles = process.env.ProgramFiles || "C:\\Program Files"; + const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)"; + const localAppData = process.env.LOCALAPPDATA || ""; + + const installed = [ + path.join(programFiles, "WinRAR", "UnRAR.exe"), + path.join(programFiles, "WinRAR", "WinRAR.exe"), + path.join(programFilesX86, "WinRAR", "UnRAR.exe"), + path.join(programFilesX86, "WinRAR", "WinRAR.exe") + ]; + + if (localAppData) { + installed.push(path.join(localAppData, "Programs", "WinRAR", "UnRAR.exe")); + installed.push(path.join(localAppData, "Programs", "WinRAR", "WinRAR.exe")); + } + + const ordered = resolvedExtractorCommand + ? [resolvedExtractorCommand, ...installed, "UnRAR.exe", "WinRAR.exe", "unrar", "winrar"] + : [...installed, "UnRAR.exe", "WinRAR.exe", "unrar", "winrar"]; return Array.from(new Set(ordered.filter(Boolean))); } @@ -79,6 +86,10 @@ function isAbsoluteCommand(command: string): boolean { || command.includes("/"); } +function isNoExtractorError(errorText: string): boolean { + return String(errorText || "").toLowerCase().includes("nicht gefunden"); +} + type ExtractSpawnResult = { ok: boolean; missingCommand: boolean; @@ -139,10 +150,10 @@ export function buildExternalExtractArgs( ): string[] { const mode = effectiveConflictMode(conflictMode); const lower = command.toLowerCase(); - if (lower.includes("unrar")) { + if (lower.includes("unrar") || lower.includes("winrar")) { const overwrite = mode === "overwrite" ? "-o+" : mode === "rename" ? "-or" : "-o-"; const pass = password ? `-p${password}` : "-p-"; - return ["x", overwrite, pass, archivePath, `${targetDir}${path.sep}`]; + return ["x", overwrite, pass, "-y", archivePath, `${targetDir}${path.sep}`]; } const overwrite = mode === "overwrite" ? "-aoa" : mode === "rename" ? "-aou" : "-aos"; @@ -150,50 +161,54 @@ export function buildExternalExtractArgs( return ["x", "-y", overwrite, pass, archivePath, `-o${targetDir}`]; } -async function runExternalExtract(archivePath: string, targetDir: string, conflictMode: ConflictMode): Promise { - if (extractorUnavailable) { - throw new Error(extractorUnavailableReason || "Kein Entpacker gefunden (7-Zip/unrar fehlt)"); +async function resolveExtractorCommand(): Promise { + if (resolvedExtractorCommand) { + return resolvedExtractorCommand; + } + if (resolveFailureReason) { + throw new Error(resolveFailureReason); } - const candidates = extractorCandidates(); + const candidates = winRarCandidates(); + for (const command of candidates) { + if (isAbsoluteCommand(command) && !fs.existsSync(command)) { + continue; + } + const probeArgs = command.toLowerCase().includes("winrar") ? ["-?"] : ["?"]; + const probe = await runExtractCommand(command, probeArgs); + if (!probe.missingCommand) { + resolvedExtractorCommand = command; + resolveFailureReason = ""; + logger.info(`Entpacker erkannt: ${command}`); + return command; + } + } + + resolveFailureReason = NO_EXTRACTOR_MESSAGE; + throw new Error(resolveFailureReason); +} + +async function runExternalExtract(archivePath: string, targetDir: string, conflictMode: ConflictMode): Promise { + const command = await resolveExtractorCommand(); const passwords = archivePasswords(); let lastError = ""; - let sawExecutableCommand = false; - let missingCommands = 0; fs.mkdirSync(targetDir, { recursive: true }); - for (const command of candidates) { - if (isAbsoluteCommand(command) && !fs.existsSync(command)) { - missingCommands += 1; - continue; + for (const password of passwords) { + const args = buildExternalExtractArgs(command, archivePath, targetDir, conflictMode, password); + const result = await runExtractCommand(command, args); + if (result.ok) { + return; } - for (const password of passwords) { - const args = buildExternalExtractArgs(command, archivePath, targetDir, conflictMode, password); - const result = await runExtractCommand(command, args); - if (result.ok) { - preferredExtractorCommand = command; - extractorUnavailable = false; - extractorUnavailableReason = ""; - return; - } - - if (result.missingCommand) { - missingCommands += 1; - lastError = result.errorText; - break; - } - - sawExecutableCommand = true; - lastError = result.errorText; + if (result.missingCommand) { + resolvedExtractorCommand = null; + resolveFailureReason = NO_EXTRACTOR_MESSAGE; + throw new Error(NO_EXTRACTOR_MESSAGE); } - } - if (!sawExecutableCommand && missingCommands >= candidates.length) { - extractorUnavailable = true; - extractorUnavailableReason = "Kein Entpacker gefunden (7-Zip/unrar fehlt oder konnte nicht gestartet werden)"; - throw new Error(extractorUnavailableReason); + lastError = result.errorText; } throw new Error(lastError || "Entpacken fehlgeschlagen"); @@ -272,6 +287,13 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{ const errorText = String(error); lastError = errorText; logger.error(`Entpack-Fehler ${path.basename(archivePath)}: ${errorText}`); + if (isNoExtractorError(errorText)) { + const remaining = candidates.length - (extracted + failed); + if (remaining > 0) { + failed += remaining; + } + break; + } } } diff --git a/tests/extractor.test.ts b/tests/extractor.test.ts index 3b69b58..42f3fd1 100644 --- a/tests/extractor.test.ts +++ b/tests/extractor.test.ts @@ -15,36 +15,29 @@ afterEach(() => { describe("extractor", () => { it("maps external extractor args by conflict mode", () => { - expect(buildExternalExtractArgs("7z", "archive.rar", "C:\\target", "overwrite")).toEqual([ + expect(buildExternalExtractArgs("WinRAR.exe", "archive.rar", "C:\\target", "overwrite")).toEqual([ "x", + "-o+", + "-p-", "-y", - "-aoa", - "-p", "archive.rar", - "-oC:\\target" + "C:\\target\\" ]); - expect(buildExternalExtractArgs("7z", "archive.rar", "C:\\target", "ask")).toEqual([ + expect(buildExternalExtractArgs("WinRAR.exe", "archive.rar", "C:\\target", "ask", "serienfans.org")).toEqual([ "x", - "-y", - "-aos", - "-p", - "archive.rar", - "-oC:\\target" - ]); - expect(buildExternalExtractArgs("7z", "archive.rar", "C:\\target", "ask", "serienfans.org")).toEqual([ - "x", - "-y", - "-aos", + "-o-", "-pserienfans.org", + "-y", "archive.rar", - "-oC:\\target" + "C:\\target\\" ]); const unrarRename = buildExternalExtractArgs("unrar", "archive.rar", "C:\\target", "rename"); expect(unrarRename[0]).toBe("x"); expect(unrarRename[1]).toBe("-or"); expect(unrarRename[2]).toBe("-p-"); - expect(unrarRename[3]).toBe("archive.rar"); + expect(unrarRename[3]).toBe("-y"); + expect(unrarRename[4]).toBe("archive.rar"); }); it("deletes only successfully extracted archives", async () => {