Replace 7zip-bin with WinRAR for archive extraction v1.3.0

- Remove 7zip-bin dependency and asarUnpack config
- Use WinRAR/UnRAR.exe from standard install paths with auto-detection
- Add resolver with probing to cache the found extractor command
- Add -y flag for auto-confirm and WinRAR.exe command support
- Update tests for WinRAR argument format

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-02-27 13:34:34 +01:00
parent d867c55e37
commit 73c8b6e670
4 changed files with 88 additions and 77 deletions

6
package-lock.json generated
View File

@ -1,15 +1,14 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.1.29", "version": "1.3.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.1.29", "version": "1.3.0",
"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",
@ -2231,6 +2230,7 @@
"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.29", "version": "1.3.0",
"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,12 +22,11 @@
"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": {
"@types/node": "^24.0.13",
"@types/adm-zip": "^0.5.7", "@types/adm-zip": "^0.5.7",
"@types/node": "^24.0.13",
"@types/react": "^18.3.12", "@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1", "@types/react-dom": "^18.3.1",
"@types/uuid": "^10.0.0", "@types/uuid": "^10.0.0",
@ -55,9 +54,6 @@
"build/renderer/**/*", "build/renderer/**/*",
"package.json" "package.json"
], ],
"asarUnpack": [
"node_modules/7zip-bin/**/*"
],
"win": { "win": {
"target": [ "target": [
"nsis", "nsis",

View File

@ -2,17 +2,15 @@ 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"]; const NO_EXTRACTOR_MESSAGE = "WinRAR/UnRAR nicht gefunden. Bitte WinRAR installieren.";
let preferredExtractorCommand: string | null = null; let resolvedExtractorCommand: string | null = null;
let extractorUnavailable = false; let resolveFailureReason = "";
let extractorUnavailableReason = "";
export interface ExtractOptions { export interface ExtractOptions {
packageDir: string; packageDir: string;
@ -51,12 +49,6 @@ function cleanErrorText(text: string): string {
return String(text || "").replace(/\s+/g, " ").trim().slice(0, 240); 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[] { function archivePasswords(): string[] {
const custom = String(process.env.RD_ARCHIVE_PASSWORDS || "") const custom = String(process.env.RD_ARCHIVE_PASSWORDS || "")
.split(/[;,\n]/g) .split(/[;,\n]/g)
@ -65,11 +57,26 @@ function archivePasswords(): string[] {
return Array.from(new Set([...DEFAULT_ARCHIVE_PASSWORDS, ...custom])); return Array.from(new Set([...DEFAULT_ARCHIVE_PASSWORDS, ...custom]));
} }
function extractorCandidates(): string[] { function winRarCandidates(): string[] {
const bundled = normalizeBundledExtractorPath(path7za); const programFiles = process.env.ProgramFiles || "C:\\Program Files";
const ordered = preferredExtractorCommand const programFilesX86 = process.env["ProgramFiles(x86)"] || "C:\\Program Files (x86)";
? [preferredExtractorCommand, bundled, ...FALLBACK_COMMANDS] const localAppData = process.env.LOCALAPPDATA || "";
: [bundled, ...FALLBACK_COMMANDS];
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))); return Array.from(new Set(ordered.filter(Boolean)));
} }
@ -79,6 +86,10 @@ function isAbsoluteCommand(command: string): boolean {
|| command.includes("/"); || command.includes("/");
} }
function isNoExtractorError(errorText: string): boolean {
return String(errorText || "").toLowerCase().includes("nicht gefunden");
}
type ExtractSpawnResult = { type ExtractSpawnResult = {
ok: boolean; ok: boolean;
missingCommand: boolean; missingCommand: boolean;
@ -139,10 +150,10 @@ export function buildExternalExtractArgs(
): string[] { ): string[] {
const mode = effectiveConflictMode(conflictMode); const mode = effectiveConflictMode(conflictMode);
const lower = command.toLowerCase(); 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 overwrite = mode === "overwrite" ? "-o+" : mode === "rename" ? "-or" : "-o-";
const pass = password ? `-p${password}` : "-p-"; 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"; const overwrite = mode === "overwrite" ? "-aoa" : mode === "rename" ? "-aou" : "-aos";
@ -150,50 +161,54 @@ export function buildExternalExtractArgs(
return ["x", "-y", overwrite, pass, archivePath, `-o${targetDir}`]; return ["x", "-y", overwrite, pass, archivePath, `-o${targetDir}`];
} }
async function runExternalExtract(archivePath: string, targetDir: string, conflictMode: ConflictMode): Promise<void> { async function resolveExtractorCommand(): Promise<string> {
if (extractorUnavailable) { if (resolvedExtractorCommand) {
throw new Error(extractorUnavailableReason || "Kein Entpacker gefunden (7-Zip/unrar fehlt)"); 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<void> {
const command = await resolveExtractorCommand();
const passwords = archivePasswords(); const passwords = archivePasswords();
let lastError = ""; let lastError = "";
let sawExecutableCommand = false;
let missingCommands = 0;
fs.mkdirSync(targetDir, { recursive: true }); fs.mkdirSync(targetDir, { recursive: true });
for (const command of candidates) { for (const password of passwords) {
if (isAbsoluteCommand(command) && !fs.existsSync(command)) { const args = buildExternalExtractArgs(command, archivePath, targetDir, conflictMode, password);
missingCommands += 1; const result = await runExtractCommand(command, args);
continue; if (result.ok) {
return;
} }
for (const password of passwords) { if (result.missingCommand) {
const args = buildExternalExtractArgs(command, archivePath, targetDir, conflictMode, password); resolvedExtractorCommand = null;
const result = await runExtractCommand(command, args); resolveFailureReason = NO_EXTRACTOR_MESSAGE;
if (result.ok) { throw new Error(NO_EXTRACTOR_MESSAGE);
preferredExtractorCommand = command;
extractorUnavailable = false;
extractorUnavailableReason = "";
return;
}
if (result.missingCommand) {
missingCommands += 1;
lastError = result.errorText;
break;
}
sawExecutableCommand = true;
lastError = result.errorText;
} }
}
if (!sawExecutableCommand && missingCommands >= candidates.length) { lastError = result.errorText;
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"); throw new Error(lastError || "Entpacken fehlgeschlagen");
@ -272,6 +287,13 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
const errorText = String(error); const errorText = String(error);
lastError = errorText; lastError = errorText;
logger.error(`Entpack-Fehler ${path.basename(archivePath)}: ${errorText}`); logger.error(`Entpack-Fehler ${path.basename(archivePath)}: ${errorText}`);
if (isNoExtractorError(errorText)) {
const remaining = candidates.length - (extracted + failed);
if (remaining > 0) {
failed += remaining;
}
break;
}
} }
} }

View File

@ -15,36 +15,29 @@ afterEach(() => {
describe("extractor", () => { describe("extractor", () => {
it("maps external extractor args by conflict mode", () => { 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", "x",
"-o+",
"-p-",
"-y", "-y",
"-aoa",
"-p",
"archive.rar", "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", "x",
"-y", "-o-",
"-aos",
"-p",
"archive.rar",
"-oC:\\target"
]);
expect(buildExternalExtractArgs("7z", "archive.rar", "C:\\target", "ask", "serienfans.org")).toEqual([
"x",
"-y",
"-aos",
"-pserienfans.org", "-pserienfans.org",
"-y",
"archive.rar", "archive.rar",
"-oC:\\target" "C:\\target\\"
]); ]);
const unrarRename = buildExternalExtractArgs("unrar", "archive.rar", "C:\\target", "rename"); const unrarRename = buildExternalExtractArgs("unrar", "archive.rar", "C:\\target", "rename");
expect(unrarRename[0]).toBe("x"); expect(unrarRename[0]).toBe("x");
expect(unrarRename[1]).toBe("-or"); expect(unrarRename[1]).toBe("-or");
expect(unrarRename[2]).toBe("-p-"); 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 () => { it("deletes only successfully extracted archives", async () => {