Fix extractor ENOENT stalls and add built-in archive passwords
Some checks are pending
Build and Release / build (push) Waiting to run

This commit is contained in:
Sucukdeluxe 2026-02-27 12:53:56 +01:00
parent 741a0d67cc
commit d867c55e37
4 changed files with 141 additions and 50 deletions

6
package-lock.json generated
View File

@ -1,14 +1,15 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.1.28", "version": "1.1.29",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.1.28", "version": "1.1.29",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"7zip-bin": "^5.2.0",
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@ -2230,7 +2231,6 @@
"version": "5.2.0", "version": "5.2.0",
"resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz", "resolved": "https://registry.npmjs.org/7zip-bin/-/7zip-bin-5.2.0.tgz",
"integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==", "integrity": "sha512-ukTPVhqG4jNzMro2qA9HSCSSVJN3aN7tlb+hfqYCt3ER0yWroeA2VR38MNrOHLQ/cVj+DaIMad0kFCtWWowh/A==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/abbrev": { "node_modules/abbrev": {

View File

@ -1,6 +1,6 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.1.28", "version": "1.1.29",
"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",
@ -22,6 +22,7 @@
"adm-zip": "^0.5.16", "adm-zip": "^0.5.16",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"7zip-bin": "^5.2.0",
"uuid": "^11.1.0" "uuid": "^11.1.0"
}, },
"devDependencies": { "devDependencies": {
@ -54,6 +55,9 @@
"build/renderer/**/*", "build/renderer/**/*",
"package.json" "package.json"
], ],
"asarUnpack": [
"node_modules/7zip-bin/**/*"
],
"win": { "win": {
"target": [ "target": [
"nsis", "nsis",

View File

@ -3,7 +3,7 @@ import os from "node:os";
import { AppSettings } from "../shared/types"; import { AppSettings } from "../shared/types";
export const APP_NAME = "Debrid Download Manager"; 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 API_BASE_URL = "https://api.real-debrid.com/rest/1.0";
export const DCRYPT_UPLOAD_URL = "https://dcrypt.it/decrypt/upload"; export const DCRYPT_UPLOAD_URL = "https://dcrypt.it/decrypt/upload";

View File

@ -2,11 +2,17 @@ import fs from "node:fs";
import path from "node:path"; import path from "node:path";
import { spawn } from "node:child_process"; import { spawn } from "node:child_process";
import AdmZip from "adm-zip"; import AdmZip from "adm-zip";
import { path7za } from "7zip-bin";
import { CleanupMode, ConflictMode } from "../shared/types"; import { CleanupMode, ConflictMode } from "../shared/types";
import { logger } from "./logger"; import { logger } from "./logger";
import { removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup"; import { removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup";
const DEFAULT_ARCHIVE_PASSWORDS = ["", "serienfans.org", "serienjunkies.org"]; 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 { export interface ExtractOptions {
packageDir: string; packageDir: string;
@ -41,6 +47,89 @@ function effectiveConflictMode(conflictMode: ConflictMode): "overwrite" | "skip"
return "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<ExtractSpawnResult> {
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( export function buildExternalExtractArgs(
command: string, command: string,
archivePath: string, archivePath: string,
@ -61,55 +150,53 @@ export function buildExternalExtractArgs(
return ["x", "-y", overwrite, pass, archivePath, `-o${targetDir}`]; return ["x", "-y", overwrite, pass, archivePath, `-o${targetDir}`];
} }
function runExternalExtract(archivePath: string, targetDir: string, conflictMode: ConflictMode): Promise<void> { async function runExternalExtract(archivePath: string, targetDir: string, conflictMode: ConflictMode): Promise<void> {
const candidates = ["7z", "C:\\Program Files\\7-Zip\\7z.exe", "C:\\Program Files (x86)\\7-Zip\\7z.exe", "unrar"]; if (extractorUnavailable) {
const passwords = Array.from(new Set(DEFAULT_ARCHIVE_PASSWORDS)); throw new Error(extractorUnavailableReason || "Kein Entpacker gefunden (7-Zip/unrar fehlt)");
return new Promise((resolve, reject) => { }
let lastError = "";
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 => { fs.mkdirSync(targetDir, { recursive: true });
if (cmdIdx >= candidates.length) {
reject(new Error(lastError || "Kein 7z/unrar gefunden")); 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; return;
} }
fs.mkdirSync(targetDir, { recursive: true });
const cmd = candidates[cmdIdx]; if (result.missingCommand) {
const password = passwords[passwordIdx] || ""; missingCommands += 1;
const args = buildExternalExtractArgs(cmd, archivePath, targetDir, conflictMode, password); lastError = result.errorText;
const child = spawn(cmd, args, { windowsHide: true }); break;
let output = ""; }
child.stdout.on("data", (chunk) => {
output += String(chunk || ""); sawExecutableCommand = true;
}); lastError = result.errorText;
child.stderr.on("data", (chunk) => { }
output += String(chunk || ""); }
});
child.on("error", (error) => { if (!sawExecutableCommand && missingCommands >= candidates.length) {
lastError = cleanErrorText(String(error)); extractorUnavailable = true;
tryExec(cmdIdx + 1, 0); extractorUnavailableReason = "Kein Entpacker gefunden (7-Zip/unrar fehlt oder konnte nicht gestartet werden)";
}); throw new Error(extractorUnavailableReason);
child.on("close", (code) => { }
if (code === 0 || code === 1) {
resolve(); throw new Error(lastError || "Entpacken fehlgeschlagen");
} 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);
});
} }
function extractZipArchive(archivePath: string, targetDir: string, conflictMode: ConflictMode): void { function extractZipArchive(archivePath: string, targetDir: string, conflictMode: ConflictMode): void {