diff --git a/package-lock.json b/package-lock.json index b723af2..0f26d38 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,15 @@ { "name": "real-debrid-downloader", - "version": "1.1.28", + "version": "1.1.29", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "real-debrid-downloader", - "version": "1.1.28", + "version": "1.1.29", "license": "MIT", "dependencies": { + "7zip-bin": "^5.2.0", "adm-zip": "^0.5.16", "react": "^18.3.1", "react-dom": "^18.3.1", @@ -2230,7 +2231,6 @@ "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 cdafa51..b1fde0d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.1.28", + "version": "1.1.29", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "main": "build/main/main/main.js", "author": "Sucukdeluxe", @@ -22,6 +22,7 @@ "adm-zip": "^0.5.16", "react": "^18.3.1", "react-dom": "^18.3.1", + "7zip-bin": "^5.2.0", "uuid": "^11.1.0" }, "devDependencies": { @@ -54,6 +55,9 @@ "build/renderer/**/*", "package.json" ], + "asarUnpack": [ + "node_modules/7zip-bin/**/*" + ], "win": { "target": [ "nsis", diff --git a/src/main/constants.ts b/src/main/constants.ts index d9a7217..6100318 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -3,7 +3,7 @@ import os from "node:os"; import { AppSettings } from "../shared/types"; export const APP_NAME = "Debrid Download Manager"; -export const APP_VERSION = "1.1.28"; +export const APP_VERSION = "1.1.29"; export const API_BASE_URL = "https://api.real-debrid.com/rest/1.0"; export const DCRYPT_UPLOAD_URL = "https://dcrypt.it/decrypt/upload"; diff --git a/src/main/extractor.ts b/src/main/extractor.ts index c3e3060..424bece 100644 --- a/src/main/extractor.ts +++ b/src/main/extractor.ts @@ -2,11 +2,17 @@ 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"]; + +let preferredExtractorCommand: string | null = null; +let extractorUnavailable = false; +let extractorUnavailableReason = ""; export interface ExtractOptions { packageDir: string; @@ -41,6 +47,89 @@ function effectiveConflictMode(conflictMode: ConflictMode): "overwrite" | "skip" return "skip"; } +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) + .map((part) => part.trim()) + .filter(Boolean); + 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]; + return Array.from(new Set(ordered.filter(Boolean))); +} + +function isAbsoluteCommand(command: string): boolean { + return path.isAbsolute(command) + || command.includes("\\") + || command.includes("/"); +} + +type ExtractSpawnResult = { + ok: boolean; + missingCommand: boolean; + errorText: string; +}; + +function runExtractCommand(command: string, args: string[]): Promise { + return new Promise((resolve) => { + let settled = false; + let output = ""; + const child = spawn(command, args, { windowsHide: true }); + + child.stdout.on("data", (chunk) => { + output += String(chunk || ""); + }); + child.stderr.on("data", (chunk) => { + output += String(chunk || ""); + }); + + child.on("error", (error) => { + if (settled) { + return; + } + settled = true; + const text = cleanErrorText(String(error)); + resolve({ + ok: false, + missingCommand: text.toLowerCase().includes("enoent"), + errorText: text + }); + }); + + child.on("close", (code) => { + if (settled) { + return; + } + settled = true; + if (code === 0 || code === 1) { + resolve({ ok: true, missingCommand: false, errorText: "" }); + return; + } + const cleaned = cleanErrorText(output); + resolve({ + ok: false, + missingCommand: false, + errorText: cleaned || `Exit Code ${String(code ?? "?")}` + }); + }); + }); +} + export function buildExternalExtractArgs( command: string, archivePath: string, @@ -61,55 +150,53 @@ export function buildExternalExtractArgs( return ["x", "-y", overwrite, pass, archivePath, `-o${targetDir}`]; } -function runExternalExtract(archivePath: string, targetDir: string, conflictMode: ConflictMode): Promise { - const candidates = ["7z", "C:\\Program Files\\7-Zip\\7z.exe", "C:\\Program Files (x86)\\7-Zip\\7z.exe", "unrar"]; - const passwords = Array.from(new Set(DEFAULT_ARCHIVE_PASSWORDS)); - return new Promise((resolve, reject) => { - let lastError = ""; +async function runExternalExtract(archivePath: string, targetDir: string, conflictMode: ConflictMode): Promise { + if (extractorUnavailable) { + throw new Error(extractorUnavailableReason || "Kein Entpacker gefunden (7-Zip/unrar fehlt)"); + } - const cleanErrorText = (text: string): string => text.replace(/\s+/g, " ").trim().slice(0, 240); + const candidates = extractorCandidates(); + const passwords = archivePasswords(); + let lastError = ""; + let sawExecutableCommand = false; + let missingCommands = 0; - const tryExec = (cmdIdx: number, passwordIdx: number): void => { - if (cmdIdx >= candidates.length) { - reject(new Error(lastError || "Kein 7z/unrar gefunden")); + 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) { + preferredExtractorCommand = command; + extractorUnavailable = false; + extractorUnavailableReason = ""; return; } - fs.mkdirSync(targetDir, { recursive: true }); - const cmd = candidates[cmdIdx]; - const password = passwords[passwordIdx] || ""; - const args = buildExternalExtractArgs(cmd, archivePath, targetDir, conflictMode, password); - const child = spawn(cmd, args, { windowsHide: true }); - let output = ""; - child.stdout.on("data", (chunk) => { - output += String(chunk || ""); - }); - child.stderr.on("data", (chunk) => { - output += String(chunk || ""); - }); - child.on("error", (error) => { - lastError = cleanErrorText(String(error)); - tryExec(cmdIdx + 1, 0); - }); - child.on("close", (code) => { - if (code === 0 || code === 1) { - resolve(); - } else { - const cleaned = cleanErrorText(output); - if (cleaned) { - lastError = cleaned; - } else { - lastError = `Exit Code ${String(code ?? "?")}`; - } - if (passwordIdx + 1 < passwords.length) { - tryExec(cmdIdx, passwordIdx + 1); - return; - } - tryExec(cmdIdx + 1, 0); - } - }); - }; - tryExec(0, 0); - }); + + if (result.missingCommand) { + missingCommands += 1; + lastError = result.errorText; + break; + } + + sawExecutableCommand = true; + lastError = result.errorText; + } + } + + if (!sawExecutableCommand && missingCommands >= candidates.length) { + extractorUnavailable = true; + extractorUnavailableReason = "Kein Entpacker gefunden (7-Zip/unrar fehlt oder konnte nicht gestartet werden)"; + throw new Error(extractorUnavailableReason); + } + + throw new Error(lastError || "Entpacken fehlgeschlagen"); } function extractZipArchive(archivePath: string, targetDir: string, conflictMode: ConflictMode): void {