Fix extractor ENOENT stalls and add built-in archive passwords
Some checks are pending
Build and Release / build (push) Waiting to run
Some checks are pending
Build and Release / build (push) Waiting to run
This commit is contained in:
parent
741a0d67cc
commit
d867c55e37
6
package-lock.json
generated
6
package-lock.json
generated
@ -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": {
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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";
|
||||||
|
|||||||
@ -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) => {
|
}
|
||||||
|
|
||||||
|
const candidates = extractorCandidates();
|
||||||
|
const passwords = archivePasswords();
|
||||||
let lastError = "";
|
let lastError = "";
|
||||||
|
let sawExecutableCommand = false;
|
||||||
|
let missingCommands = 0;
|
||||||
|
|
||||||
const cleanErrorText = (text: string): string => text.replace(/\s+/g, " ").trim().slice(0, 240);
|
|
||||||
|
|
||||||
const tryExec = (cmdIdx: number, passwordIdx: number): void => {
|
|
||||||
if (cmdIdx >= candidates.length) {
|
|
||||||
reject(new Error(lastError || "Kein 7z/unrar gefunden"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
fs.mkdirSync(targetDir, { recursive: true });
|
fs.mkdirSync(targetDir, { recursive: true });
|
||||||
const cmd = candidates[cmdIdx];
|
|
||||||
const password = passwords[passwordIdx] || "";
|
for (const command of candidates) {
|
||||||
const args = buildExternalExtractArgs(cmd, archivePath, targetDir, conflictMode, password);
|
if (isAbsoluteCommand(command) && !fs.existsSync(command)) {
|
||||||
const child = spawn(cmd, args, { windowsHide: true });
|
missingCommands += 1;
|
||||||
let output = "";
|
continue;
|
||||||
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);
|
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;
|
||||||
}
|
}
|
||||||
tryExec(cmdIdx + 1, 0);
|
|
||||||
|
if (result.missingCommand) {
|
||||||
|
missingCommands += 1;
|
||||||
|
lastError = result.errorText;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
};
|
sawExecutableCommand = true;
|
||||||
tryExec(0, 0);
|
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 {
|
function extractZipArchive(archivePath: string, targetDir: string, conflictMode: ConflictMode): void {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user