Compare commits

..

No commits in common. "1222cb08b5ea038e3c46b6465259f1932d99b273" and "a322a16b7b1f3cd2053caddfab57ce3d4ec7fb5d" have entirely different histories.

5 changed files with 83 additions and 365 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.7.30", "version": "1.7.29",
"description": "Desktop downloader", "description": "Desktop downloader",
"main": "build/main/main/main.js", "main": "build/main/main/main.js",
"author": "Sucukdeluxe", "author": "Sucukdeluxe",

View File

@ -42,7 +42,7 @@ function releaseTlsSkip(): void {
} }
import { cleanupCancelledPackageArtifactsAsync, removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup"; import { cleanupCancelledPackageArtifactsAsync, removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup";
import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkRapidgatorOnline, fetchAllDebridHostInfo, getAvailableDebridLinkApiKeys } from "./debrid"; import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkRapidgatorOnline, fetchAllDebridHostInfo, getAvailableDebridLinkApiKeys } from "./debrid";
import { cleanupArchives, clearExtractResumeState, collectArchiveCleanupTargets, extractPackageArchives, findArchiveCandidates, hasAnyFilesRecursive, removeEmptyDirectoryTree, type ExtractArchiveFailureInfo } from "./extractor"; import { cleanupArchives, clearExtractResumeState, collectArchiveCleanupTargets, extractPackageArchives, findArchiveCandidates, hasAnyFilesRecursive, removeEmptyDirectoryTree } from "./extractor";
import { validateFileAgainstManifest } from "./integrity"; import { validateFileAgainstManifest } from "./integrity";
import { logger } from "./logger"; import { logger } from "./logger";
import { StoragePaths, saveSession, saveSessionAsync, saveSettings, saveSettingsAsync } from "./storage"; import { StoragePaths, saveSession, saveSessionAsync, saveSettings, saveSettingsAsync } from "./storage";
@ -4057,62 +4057,6 @@ export class DownloadManager extends EventEmitter {
} }
} }
private autoRecoverArchiveCrcFailure(
pkg: PackageEntry,
items: DownloadItem[],
failure: ExtractArchiveFailureInfo,
scope: "hybrid" | "full"
): number {
if (!failure.suggestRedownload || failure.category !== "crc_error") {
return 0;
}
const archiveItems = resolveArchiveItemsFromList(failure.archiveName, items)
.filter((item) => item.status === "completed");
if (archiveItems.length === 0) {
logger.warn(`Auto-Recovery (${scope}): Keine completed Items für ${failure.archiveName} gefunden, überspringe`);
return 0;
}
const queuedAt = nowMs();
const reason = "Wartet (Auto-Recovery: Archiv beschädigt/unvollständig)";
let changed = 0;
for (const item of archiveItems) {
const claimedTargetPath = String(item.targetPath || "").trim();
if (claimedTargetPath) {
try {
fs.rmSync(claimedTargetPath, { force: true });
} catch {
// ignore; claim is still released so a fresh path can be chosen if needed
}
}
this.releaseTargetPath(item.id);
this.dropItemContribution(item.id);
item.targetPath = "";
item.status = "queued";
item.attempts = 0;
item.downloadedBytes = 0;
item.progressPercent = 0;
item.speedBps = 0;
item.lastError = failure.errorText;
item.fullStatus = reason;
item.updatedAt = queuedAt;
changed += 1;
}
if (changed > 0) {
pkg.status = (pkg.enabled && this.session.running && !this.session.paused) ? "downloading" : "queued";
pkg.updatedAt = queuedAt;
logger.warn(
`Auto-Recovery (${scope}): ${failure.archiveName} auf queued gesetzt (${changed} Items), ` +
`reason=${compactErrorText(failure.jvmFailureReason || failure.errorText)}`
);
this.persistSoon();
this.emitState();
}
return changed;
}
/** Detect items whose targetPath has a " (N)" suffix from a previous bug and rename /** Detect items whose targetPath has a " (N)" suffix from a previous bug and rename
* them back to the original filename if the original path is not claimed by another item. */ * them back to the original filename if the original path is not claimed by another item. */
private fixDuplicateSuffixFiles(): void { private fixDuplicateSuffixFiles(): void {
@ -7254,7 +7198,6 @@ export class DownloadManager extends EventEmitter {
resolveArchiveItemsFromList(archiveName, items); resolveArchiveItemsFromList(archiveName, items);
// Track archives for parallel hybrid extraction progress // Track archives for parallel hybrid extraction progress
const autoRecoveredArchives = new Set<string>();
const hybridResolvedItems = new Map<string, DownloadItem[]>(); const hybridResolvedItems = new Map<string, DownloadItem[]>();
const hybridStartTimes = new Map<string, number>(); const hybridStartTimes = new Map<string, number>();
let hybridLastEmitAt = 0; let hybridLastEmitAt = 0;
@ -7299,15 +7242,6 @@ export class DownloadManager extends EventEmitter {
hybridMode: true, hybridMode: true,
maxParallel: this.settings.maxParallelExtract || 2, maxParallel: this.settings.maxParallelExtract || 2,
extractCpuPriority: "high", extractCpuPriority: "high",
onArchiveFailure: (failure) => {
if (autoRecoveredArchives.has(failure.archiveName)) {
return;
}
const changed = this.autoRecoverArchiveCrcFailure(pkg, items, failure, "hybrid");
if (changed > 0) {
autoRecoveredArchives.add(failure.archiveName);
}
},
onProgress: (progress) => { onProgress: (progress) => {
if (progress.phase === "preparing") { if (progress.phase === "preparing") {
pkg.postProcessLabel = progress.archiveName || "Vorbereiten..."; pkg.postProcessLabel = progress.archiveName || "Vorbereiten...";
@ -7339,9 +7273,6 @@ export class DownloadManager extends EventEmitter {
const initLabel = `Entpacken 0% · ${progress.archiveName}`; const initLabel = `Entpacken 0% · ${progress.archiveName}`;
const initAt = nowMs(); const initAt = nowMs();
for (const entry of resolved) { for (const entry of resolved) {
if (entry.status !== "completed" || isExtractedLabel(entry.fullStatus)) {
continue;
}
if (!isExtractedLabel(entry.fullStatus)) { if (!isExtractedLabel(entry.fullStatus)) {
entry.fullStatus = initLabel; entry.fullStatus = initLabel;
entry.updatedAt = initAt; entry.updatedAt = initAt;
@ -7362,9 +7293,10 @@ export class DownloadManager extends EventEmitter {
? "Entpacken - Error" ? "Entpacken - Error"
: formatExtractDone(doneAt - startedAt); : formatExtractDone(doneAt - startedAt);
for (const entry of archItems) { for (const entry of archItems) {
if (entry.status !== "completed" || isExtractedLabel(entry.fullStatus)) continue; if (!isExtractedLabel(entry.fullStatus)) {
entry.fullStatus = doneLabel; entry.fullStatus = doneLabel;
entry.updatedAt = doneAt; entry.updatedAt = doneAt;
}
} }
hybridResolvedItems.delete(progress.archiveName); hybridResolvedItems.delete(progress.archiveName);
hybridStartTimes.delete(progress.archiveName); hybridStartTimes.delete(progress.archiveName);
@ -7392,9 +7324,10 @@ export class DownloadManager extends EventEmitter {
} }
const updatedAt = nowMs(); const updatedAt = nowMs();
for (const entry of archItems) { for (const entry of archItems) {
if (entry.status !== "completed" || isExtractedLabel(entry.fullStatus) || entry.fullStatus === label) continue; if (!isExtractedLabel(entry.fullStatus) && entry.fullStatus !== label) {
entry.fullStatus = label; entry.fullStatus = label;
entry.updatedAt = updatedAt; entry.updatedAt = updatedAt;
}
} }
} }
} }
@ -7463,7 +7396,7 @@ export class DownloadManager extends EventEmitter {
// downloading) as "Done". // downloading) as "Done".
const updatedAt = nowMs(); const updatedAt = nowMs();
for (const entry of hybridItems) { for (const entry of hybridItems) {
if (entry.status !== "completed" || isExtractedLabel(entry.fullStatus)) { if (isExtractedLabel(entry.fullStatus)) {
continue; continue;
} }
const status = entry.fullStatus || ""; const status = entry.fullStatus || "";
@ -7484,7 +7417,7 @@ export class DownloadManager extends EventEmitter {
logger.info(`Hybrid-Extract abgebrochen: pkg=${pkg.name}`); logger.info(`Hybrid-Extract abgebrochen: pkg=${pkg.name}`);
const abortAt = nowMs(); const abortAt = nowMs();
for (const entry of hybridItems) { for (const entry of hybridItems) {
if (entry.status !== "completed" || isExtractedLabel(entry.fullStatus || "")) continue; if (isExtractedLabel(entry.fullStatus || "")) continue;
if (/^Entpacken\b/i.test(entry.fullStatus || "") || /^Passwort\b/i.test(entry.fullStatus || "")) { if (/^Entpacken\b/i.test(entry.fullStatus || "") || /^Passwort\b/i.test(entry.fullStatus || "")) {
entry.fullStatus = "Entpacken abgebrochen (wird fortgesetzt)"; entry.fullStatus = "Entpacken abgebrochen (wird fortgesetzt)";
entry.updatedAt = abortAt; entry.updatedAt = abortAt;
@ -7495,7 +7428,7 @@ export class DownloadManager extends EventEmitter {
logger.warn(`Hybrid-Extract Fehler: pkg=${pkg.name}, reason=${compactErrorText(error)}`); logger.warn(`Hybrid-Extract Fehler: pkg=${pkg.name}, reason=${compactErrorText(error)}`);
const errorAt = nowMs(); const errorAt = nowMs();
for (const entry of hybridItems) { for (const entry of hybridItems) {
if (entry.status !== "completed" || isExtractedLabel(entry.fullStatus || "")) continue; if (isExtractedLabel(entry.fullStatus || "")) continue;
if (/^Entpacken\b/i.test(entry.fullStatus || "") || /^Passwort\b/i.test(entry.fullStatus || "")) { if (/^Entpacken\b/i.test(entry.fullStatus || "") || /^Passwort\b/i.test(entry.fullStatus || "")) {
entry.fullStatus = `Entpacken - Error`; entry.fullStatus = `Entpacken - Error`;
entry.updatedAt = errorAt; entry.updatedAt = errorAt;
@ -7676,7 +7609,6 @@ export class DownloadManager extends EventEmitter {
}, extractTimeoutMs); }, extractTimeoutMs);
try { try {
// Track archives for parallel extraction progress // Track archives for parallel extraction progress
const autoRecoveredArchives = new Set<string>();
const fullResolvedItems = new Map<string, DownloadItem[]>(); const fullResolvedItems = new Map<string, DownloadItem[]>();
const fullStartTimes = new Map<string, number>(); const fullStartTimes = new Map<string, number>();
let fullLastProgressCurrent: number | null = null; let fullLastProgressCurrent: number | null = null;
@ -7696,15 +7628,6 @@ export class DownloadManager extends EventEmitter {
// All downloads finished — use NORMAL OS priority so extraction runs at // All downloads finished — use NORMAL OS priority so extraction runs at
// full speed (matching manual 7-Zip/WinRAR speed). // full speed (matching manual 7-Zip/WinRAR speed).
extractCpuPriority: "high", extractCpuPriority: "high",
onArchiveFailure: (failure) => {
if (autoRecoveredArchives.has(failure.archiveName)) {
return;
}
const changed = this.autoRecoverArchiveCrcFailure(pkg, completedItems, failure, "full");
if (changed > 0) {
autoRecoveredArchives.add(failure.archiveName);
}
},
onProgress: (progress) => { onProgress: (progress) => {
if (progress.phase === "preparing") { if (progress.phase === "preparing") {
pkg.postProcessLabel = progress.archiveName || "Vorbereiten..."; pkg.postProcessLabel = progress.archiveName || "Vorbereiten...";
@ -7737,9 +7660,10 @@ export class DownloadManager extends EventEmitter {
const initLabel = `Entpacken 0% · ${progress.archiveName}`; const initLabel = `Entpacken 0% · ${progress.archiveName}`;
const initAt = nowMs(); const initAt = nowMs();
for (const entry of resolved) { for (const entry of resolved) {
if (entry.status !== "completed" || isExtractedLabel(entry.fullStatus)) continue; if (!isExtractedLabel(entry.fullStatus)) {
entry.fullStatus = initLabel; entry.fullStatus = initLabel;
entry.updatedAt = initAt; entry.updatedAt = initAt;
}
} }
emitExtractStatus(`Entpacken ${progress.percent}% · ${progress.archiveName}`, true); emitExtractStatus(`Entpacken ${progress.percent}% · ${progress.archiveName}`, true);
} }
@ -7755,9 +7679,10 @@ export class DownloadManager extends EventEmitter {
? "Entpacken - Error" ? "Entpacken - Error"
: formatExtractDone(doneAt - startedAt); : formatExtractDone(doneAt - startedAt);
for (const entry of archiveItems) { for (const entry of archiveItems) {
if (entry.status !== "completed" || isExtractedLabel(entry.fullStatus)) continue; if (!isExtractedLabel(entry.fullStatus)) {
entry.fullStatus = doneLabel; entry.fullStatus = doneLabel;
entry.updatedAt = doneAt; entry.updatedAt = doneAt;
}
} }
fullResolvedItems.delete(progress.archiveName); fullResolvedItems.delete(progress.archiveName);
fullStartTimes.delete(progress.archiveName); fullStartTimes.delete(progress.archiveName);
@ -7784,9 +7709,10 @@ export class DownloadManager extends EventEmitter {
} }
const updatedAt = nowMs(); const updatedAt = nowMs();
for (const entry of archiveItems) { for (const entry of archiveItems) {
if (entry.status !== "completed" || isExtractedLabel(entry.fullStatus) || entry.fullStatus === label) continue; if (!isExtractedLabel(entry.fullStatus) && entry.fullStatus !== label) {
entry.fullStatus = label; entry.fullStatus = label;
entry.updatedAt = updatedAt; entry.updatedAt = updatedAt;
}
} }
} }
} }
@ -7812,25 +7738,16 @@ export class DownloadManager extends EventEmitter {
}); });
logger.info(`Post-Processing Entpacken Ende: pkg=${pkg.name}, extracted=${result.extracted}, failed=${result.failed}, lastError=${result.lastError || ""}`); logger.info(`Post-Processing Entpacken Ende: pkg=${pkg.name}, extracted=${result.extracted}, failed=${result.failed}, lastError=${result.lastError || ""}`);
extractedCount = result.extracted; extractedCount = result.extracted;
const autoRecoveredPending = completedItems.some((item) => item.status === "queued");
// Auto-rename wird in runDeferredPostExtraction ausgeführt (im Hintergrund), // Auto-rename wird in runDeferredPostExtraction ausgeführt (im Hintergrund),
// damit der Slot sofort freigegeben wird. // damit der Slot sofort freigegeben wird.
if (autoRecoveredPending) {
pkg.postProcessLabel = undefined;
pkg.status = (pkg.enabled && this.session.running && !this.session.paused) ? "downloading" : "queued";
pkg.updatedAt = nowMs();
logger.warn(`Post-Processing: pkg=${pkg.name}, Archivfehler automatisch auf Re-Download umgestellt`);
return;
}
if (result.failed > 0) { if (result.failed > 0) {
const reason = compactErrorText(result.lastError || "Entpacken fehlgeschlagen"); const reason = compactErrorText(result.lastError || "Entpacken fehlgeschlagen");
const failAt = nowMs(); const failAt = nowMs();
for (const entry of completedItems) { for (const entry of completedItems) {
// Preserve per-archive "Entpackt - Done (X.Xs)" labels for successfully extracted archives // Preserve per-archive "Entpackt - Done (X.Xs)" labels for successfully extracted archives
if (entry.status === "completed" && !isExtractedLabel(entry.fullStatus)) { if (!isExtractedLabel(entry.fullStatus)) {
entry.fullStatus = `Entpack-Fehler: ${reason}`; entry.fullStatus = `Entpack-Fehler: ${reason}`;
entry.updatedAt = failAt; entry.updatedAt = failAt;
} }
@ -7869,7 +7786,7 @@ export class DownloadManager extends EventEmitter {
const timeoutReason = `Entpacken Timeout nach ${Math.ceil(extractTimeoutMs / 1000)}s`; const timeoutReason = `Entpacken Timeout nach ${Math.ceil(extractTimeoutMs / 1000)}s`;
logger.error(`Post-Processing Entpacken Timeout: pkg=${pkg.name}`); logger.error(`Post-Processing Entpacken Timeout: pkg=${pkg.name}`);
for (const entry of completedItems) { for (const entry of completedItems) {
if (entry.status === "completed" && !isExtractedLabel(entry.fullStatus)) { if (!isExtractedLabel(entry.fullStatus)) {
entry.fullStatus = `Entpack-Fehler: ${timeoutReason}`; entry.fullStatus = `Entpack-Fehler: ${timeoutReason}`;
entry.updatedAt = nowMs(); entry.updatedAt = nowMs();
} }
@ -7894,7 +7811,7 @@ export class DownloadManager extends EventEmitter {
const reason = compactErrorText(error); const reason = compactErrorText(error);
logger.error(`Post-Processing Entpacken Exception: pkg=${pkg.name}, reason=${reason}`); logger.error(`Post-Processing Entpacken Exception: pkg=${pkg.name}, reason=${reason}`);
for (const entry of completedItems) { for (const entry of completedItems) {
if (entry.status === "completed" && !isExtractedLabel(entry.fullStatus)) { if (!isExtractedLabel(entry.fullStatus)) {
entry.fullStatus = `Entpack-Fehler: ${reason}`; entry.fullStatus = `Entpack-Fehler: ${reason}`;
entry.updatedAt = nowMs(); entry.updatedAt = nowMs();
} }

View File

@ -110,7 +110,6 @@ export interface ExtractOptions {
hybridMode?: boolean; hybridMode?: boolean;
maxParallel?: number; maxParallel?: number;
extractCpuPriority?: string; extractCpuPriority?: string;
onArchiveFailure?: (failure: ExtractArchiveFailureInfo) => void;
} }
export interface ExtractProgressUpdate { export interface ExtractProgressUpdate {
@ -128,14 +127,6 @@ export interface ExtractProgressUpdate {
archiveSuccess?: boolean; archiveSuccess?: boolean;
} }
export interface ExtractArchiveFailureInfo {
archiveName: string;
errorText: string;
category: ExtractErrorCategory;
suggestRedownload: boolean;
jvmFailureReason?: string;
}
const MAX_EXTRACT_OUTPUT_BUFFER = 48 * 1024; const MAX_EXTRACT_OUTPUT_BUFFER = 48 * 1024;
const EXTRACT_PROGRESS_FILE = ".rd_extract_progress.json"; const EXTRACT_PROGRESS_FILE = ".rd_extract_progress.json";
const EXTRACT_BASE_TIMEOUT_MS = 6 * 60 * 1000; const EXTRACT_BASE_TIMEOUT_MS = 6 * 60 * 1000;
@ -541,26 +532,6 @@ export type ExtractErrorCategory =
| "no_extractor" | "no_extractor"
| "unknown"; | "unknown";
type ExtractionErrorWithHints = Error & {
suggestRedownload?: boolean;
jvmFailureReason?: string;
};
function withExtractionErrorHints(
error: unknown,
hints: { suggestRedownload?: boolean; jvmFailureReason?: string }
): Error {
const base = error instanceof Error ? error : new Error(String(error || "Entpacken fehlgeschlagen"));
const enhanced = base as ExtractionErrorWithHints;
if (hints.suggestRedownload) {
enhanced.suggestRedownload = true;
}
if (hints.jvmFailureReason) {
enhanced.jvmFailureReason = hints.jvmFailureReason;
}
return enhanced;
}
export function classifyExtractionError(errorText: string): ExtractErrorCategory { export function classifyExtractionError(errorText: string): ExtractErrorCategory {
const text = String(errorText || "").toLowerCase(); const text = String(errorText || "").toLowerCase();
if (text.includes("aborted:extract") || text.includes("extract_aborted")) return "aborted"; if (text.includes("aborted:extract") || text.includes("extract_aborted")) return "aborted";
@ -1175,20 +1146,6 @@ function finishDaemonRequest(result: JvmExtractResult): void {
req.resolve(result); req.resolve(result);
} }
function flushDaemonParseBuffers(req: DaemonRequest | null): void {
if (!req) {
return;
}
if (daemonStdoutBuffer.trim()) {
parseJvmLine(daemonStdoutBuffer, req.onArchiveProgress, req.parseState);
daemonStdoutBuffer = "";
}
if (daemonStderrBuffer.trim()) {
parseJvmLine(daemonStderrBuffer, req.onArchiveProgress, req.parseState);
daemonStderrBuffer = "";
}
}
function handleDaemonLine(line: string): void { function handleDaemonLine(line: string): void {
const trimmed = String(line || "").trim(); const trimmed = String(line || "").trim();
if (!trimmed) return; if (!trimmed) return;
@ -1205,41 +1162,27 @@ function handleDaemonLine(line: string): void {
const code = parseInt(trimmed.slice("RD_REQUEST_DONE ".length).trim(), 10); const code = parseInt(trimmed.slice("RD_REQUEST_DONE ".length).trim(), 10);
const req = daemonCurrentRequest; const req = daemonCurrentRequest;
if (!req) return; if (!req) return;
const finalize = (): void => { const elapsedMs = Date.now() - req.startedAt;
if (daemonCurrentRequest !== req) { logger.info(
return; `JVM Daemon Request Ende: archive=${req.archiveName}, code=${code}, ms=${elapsedMs}, pwCandidates=${req.passwordCount}, ` +
} `bestPercent=${req.parseState.bestPercent}, backend=${req.parseState.backend || "unknown"}, usedPassword=${req.parseState.usedPassword ? "yes" : "no"}`
flushDaemonParseBuffers(req); );
const elapsedMs = Date.now() - req.startedAt;
logger.info(
`JVM Daemon Request Ende: archive=${req.archiveName}, code=${code}, ms=${elapsedMs}, pwCandidates=${req.passwordCount}, ` +
`bestPercent=${req.parseState.bestPercent}, backend=${req.parseState.backend || "unknown"}, usedPassword=${req.parseState.usedPassword ? "yes" : "no"}`
);
if (code === 0) {
req.onArchiveProgress?.(100);
finishDaemonRequest({
ok: true, missingCommand: false, missingRuntime: false,
aborted: false, timedOut: false, errorText: "",
usedPassword: req.parseState.usedPassword, backend: req.parseState.backend
});
return;
}
if (code === 0) {
req.onArchiveProgress?.(100);
finishDaemonRequest({
ok: true, missingCommand: false, missingRuntime: false,
aborted: false, timedOut: false, errorText: "",
usedPassword: req.parseState.usedPassword, backend: req.parseState.backend
});
} else {
const message = cleanErrorText(req.parseState.reportedError || daemonOutput) || `Exit Code ${code}`; const message = cleanErrorText(req.parseState.reportedError || daemonOutput) || `Exit Code ${code}`;
finishDaemonRequest({ finishDaemonRequest({
ok: false, missingCommand: false, missingRuntime: isJvmRuntimeMissingError(message), ok: false, missingCommand: false, missingRuntime: isJvmRuntimeMissingError(message),
aborted: false, timedOut: false, errorText: message, aborted: false, timedOut: false, errorText: message,
usedPassword: req.parseState.usedPassword, backend: req.parseState.backend usedPassword: req.parseState.usedPassword, backend: req.parseState.backend
}); });
};
if (code !== 0 && !req.parseState.reportedError) {
setTimeout(finalize, 40);
return;
} }
finalize();
return; return;
} }
@ -1828,7 +1771,6 @@ async function runExternalExtract(
const archiveName = path.basename(archivePath); const archiveName = path.basename(archivePath);
const totalStartedAt = Date.now(); const totalStartedAt = Date.now();
let jvmFailureReason = ""; let jvmFailureReason = "";
let jvmCodecError = false;
let fallbackFromJvm = false; let fallbackFromJvm = false;
logger.info(`Extract-Backend Start: archive=${archiveName}, mode=${backendMode}, pwCandidates=${passwordCandidates.length}, timeoutMs=${timeoutMs}, hybrid=${hybridMode}`); logger.info(`Extract-Backend Start: archive=${archiveName}, mode=${backendMode}, pwCandidates=${passwordCandidates.length}, timeoutMs=${timeoutMs}, hybrid=${hybridMode}`);
@ -1885,7 +1827,6 @@ async function runExternalExtract(
const isCodecError = jvmFailureLower.includes("registered codecs") const isCodecError = jvmFailureLower.includes("registered codecs")
|| jvmFailureLower.includes("can not open") || jvmFailureLower.includes("can not open")
|| jvmFailureLower.includes("cannot open archive"); || jvmFailureLower.includes("cannot open archive");
jvmCodecError = isCodecError;
const isWrongPassword = jvmFailureReason.includes("WRONG_PASSWORD") const isWrongPassword = jvmFailureReason.includes("WRONG_PASSWORD")
|| jvmFailureLower.includes("wrong password"); || jvmFailureLower.includes("wrong password");
const shouldFallbackToLegacy = isUnsupportedMethod || isCodecError || isWrongPassword; const shouldFallbackToLegacy = isUnsupportedMethod || isCodecError || isWrongPassword;
@ -1913,61 +1854,52 @@ async function runExternalExtract(
let password: string; let password: string;
let usedCommand = command; let usedCommand = command;
try { try {
try { password = await runExternalExtractInner(
password = await runExternalExtractInner( command,
command, archivePath,
archivePath, effectiveTargetDir,
effectiveTargetDir, conflictMode,
conflictMode, passwordCandidates,
passwordCandidates, onArchiveProgress,
onArchiveProgress, signal,
signal, timeoutMs,
timeoutMs, hybridMode,
hybridMode, onPasswordAttempt,
onPasswordAttempt, forceFlatMode,
forceFlatMode, flatModeResult
flatModeResult );
); } catch (primaryError) {
} catch (primaryError) { // If the primary extractor (typically 7-Zip) fails on a RAR archive,
// If the primary extractor (typically 7-Zip) fails on a RAR archive, // try the alternative extractor (UnRAR/WinRAR) which handles RAR natively.
// try the alternative extractor (UnRAR/WinRAR) which handles RAR natively. const isRar = /\.rar$/i.test(archiveName) || /\.r\d{2,3}$/i.test(archiveName);
const isRar = /\.rar$/i.test(archiveName) || /\.r\d{2,3}$/i.test(archiveName); const errText = String((primaryError as Error)?.message || primaryError || "");
const errText = String((primaryError as Error)?.message || primaryError || ""); const isPasswordOrCorrupt = /wrong.password|checksum error|corrupt/i.test(errText);
const isPasswordOrCorrupt = /wrong.password|checksum error|corrupt/i.test(errText); if (isRar && isPasswordOrCorrupt && !signal?.aborted) {
if (isRar && isPasswordOrCorrupt && !signal?.aborted) { const alt = await findAlternativeExtractor(command);
const alt = await findAlternativeExtractor(command); if (alt) {
if (alt) { const altName = path.basename(alt).replace(/\.exe$/i, "");
const altName = path.basename(alt).replace(/\.exe$/i, ""); logger.info(`Legacy-Fallback: ${path.basename(command)} fehlgeschlagen bei RAR, versuche ${altName}: ${archiveName}`);
logger.info(`Legacy-Fallback: ${path.basename(command)} fehlgeschlagen bei RAR, versuche ${altName}: ${archiveName}`); usedCommand = alt;
usedCommand = alt; password = await runExternalExtractInner(
password = await runExternalExtractInner( alt,
alt, archivePath,
archivePath, effectiveTargetDir,
effectiveTargetDir, conflictMode,
conflictMode, passwordCandidates,
passwordCandidates, onArchiveProgress,
onArchiveProgress, signal,
signal, timeoutMs,
timeoutMs, hybridMode,
hybridMode, onPasswordAttempt,
onPasswordAttempt, forceFlatMode,
forceFlatMode, flatModeResult
flatModeResult );
);
} else {
throw primaryError;
}
} else { } else {
throw primaryError; throw primaryError;
} }
} else {
throw primaryError;
} }
} catch (legacyError) {
const legacyText = String((legacyError as Error)?.message || legacyError || "");
const suggestRedownload = jvmCodecError && classifyExtractionError(legacyText) === "crc_error";
throw withExtractionErrorHints(legacyError, {
suggestRedownload,
jvmFailureReason: jvmFailureReason || undefined
});
} }
const legacyMs = Date.now() - legacyStartedAt; const legacyMs = Date.now() - legacyStartedAt;
const extractorName = path.basename(usedCommand).replace(/\.exe$/i, ""); const extractorName = path.basename(usedCommand).replace(/\.exe$/i, "");
@ -2822,14 +2754,6 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
failed += 1; failed += 1;
lastError = errorText; lastError = errorText;
const errorCategory = classifyExtractionError(errorText); const errorCategory = classifyExtractionError(errorText);
const hintedError = error as ExtractionErrorWithHints;
options.onArchiveFailure?.({
archiveName,
errorText,
category: errorCategory,
suggestRedownload: hintedError?.suggestRedownload === true,
jvmFailureReason: hintedError?.jvmFailureReason
});
logger.error(`Entpack-Fehler ${path.basename(archivePath)} [${errorCategory}]: ${errorText}`); logger.error(`Entpack-Fehler ${path.basename(archivePath)} [${errorCategory}]: ${errorText}`);
if (errorCategory === "wrong_password" && learnedPassword) { if (errorCategory === "wrong_password" && learnedPassword) {
learnedPassword = ""; learnedPassword = "";

View File

@ -1950,98 +1950,6 @@ describe("download manager", () => {
expect(snapshot.session.packages[packageId]?.status).toBe("queued"); expect(snapshot.session.packages[packageId]?.status).toBe("queued");
}); });
it("requeues completed archive parts after auto-recovery extraction failures", () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
tempDirs.push(root);
const session = emptySession();
const packageId = "crc-pkg";
const createdAt = Date.now() - 10_000;
const outputDir = path.join(root, "downloads", "crc");
const extractDir = path.join(root, "extract", "crc");
fs.mkdirSync(outputDir, { recursive: true });
const archiveNames = ["show.s01e01.part1.rar", "show.s01e01.part2.rar"];
const itemIds = archiveNames.map((_, index) => `crc-item-${index}`);
session.packageOrder = [packageId];
session.packages[packageId] = {
id: packageId,
name: "crc",
outputDir,
extractDir,
status: "extracting",
itemIds,
cancelled: false,
enabled: true,
createdAt,
updatedAt: createdAt
};
for (const [index, archiveName] of archiveNames.entries()) {
const targetPath = path.join(outputDir, archiveName);
fs.writeFileSync(targetPath, Buffer.from(`part-${index}`));
session.items[itemIds[index]!] = {
id: itemIds[index]!,
packageId,
url: `https://dummy/${archiveName}`,
provider: "realdebrid",
status: "completed",
retries: 0,
speedBps: 0,
downloadedBytes: 4096,
totalBytes: 4096,
progressPercent: 100,
fileName: archiveName,
targetPath,
resumable: true,
attempts: 1,
lastError: "",
fullStatus: "Entpacken - Ausstehend",
createdAt,
updatedAt: createdAt
};
}
const manager = new DownloadManager(
{
...defaultSettings(),
token: "rd-token",
outputDir: path.join(root, "downloads"),
extractDir: path.join(root, "extract"),
autoExtract: true
},
session,
createStoragePaths(path.join(root, "state"))
);
const changed = (manager as any).autoRecoverArchiveCrcFailure(
session.packages[packageId],
itemIds.map((itemId) => session.items[itemId]!),
{
archiveName: "show.s01e01.part1.rar",
errorText: "Checksum error in the encrypted file",
category: "crc_error",
suggestRedownload: true,
jvmFailureReason: "Can not open the file as archive"
},
"hybrid"
);
expect(changed).toBe(2);
for (const itemId of itemIds) {
const item = session.items[itemId]!;
expect(item.status).toBe("queued");
expect(item.targetPath).toBe("");
expect(item.downloadedBytes).toBe(0);
expect(item.attempts).toBe(0);
expect(item.fullStatus).toContain("Auto-Recovery");
}
expect(fs.existsSync(path.join(outputDir, archiveNames[0]!))).toBe(false);
expect(fs.existsSync(path.join(outputDir, archiveNames[1]!))).toBe(false);
expect(session.packages[packageId]?.status).toBe("queued");
});
it("detects start conflicts when extract output already exists", async () => { it("detects start conflicts when extract output already exists", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-")); const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-dm-"));
tempDirs.push(root); tempDirs.push(root);

View File

@ -7,7 +7,6 @@ import {
buildExternalExtractArgs, buildExternalExtractArgs,
collectArchiveCleanupTargets, collectArchiveCleanupTargets,
extractPackageArchives, extractPackageArchives,
type ExtractArchiveFailureInfo,
archiveFilenamePasswords, archiveFilenamePasswords,
detectArchiveSignature, detectArchiveSignature,
classifyExtractionError, classifyExtractionError,
@ -1000,36 +999,6 @@ describe("extractor", () => {
}); });
describe("password discovery", () => { describe("password discovery", () => {
it("reports per-archive failures through onArchiveFailure", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-extract-failure-"));
tempDirs.push(root);
const packageDir = path.join(root, "pkg");
const targetDir = path.join(root, "out");
fs.mkdirSync(packageDir, { recursive: true });
fs.writeFileSync(path.join(packageDir, "broken.zip"), "not-a-zip", "utf8");
const failures: ExtractArchiveFailureInfo[] = [];
const result = await extractPackageArchives({
packageDir,
targetDir,
cleanupMode: "none",
conflictMode: "overwrite",
removeLinks: false,
removeSamples: false,
onArchiveFailure: (failure) => {
failures.push(failure);
}
});
expect(result.extracted).toBe(0);
expect(result.failed).toBe(1);
expect(failures).toHaveLength(1);
expect(failures[0]?.archiveName).toBe("broken.zip");
expect(failures[0]?.category).toBe("unsupported_format");
expect(failures[0]?.suggestRedownload).toBe(false);
});
it("extracts first archive serially before parallel pool when multiple passwords", async () => { it("extracts first archive serially before parallel pool when multiple passwords", async () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-pwdisc-")); const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-pwdisc-"));
tempDirs.push(root); tempDirs.push(root);