From 8b5c9361770450c77287c2fe546a1dc282679f31 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Fri, 27 Feb 2026 20:25:55 +0100 Subject: [PATCH] Release v1.4.11 with stability hardening and full-function regression pass --- package-lock.json | 4 +- package.json | 2 +- src/main/extractor.ts | 56 +++++++++++++++++++++++--- src/renderer/App.tsx | 93 +++++++++++++++++++++++-------------------- 4 files changed, 103 insertions(+), 52 deletions(-) diff --git a/package-lock.json b/package-lock.json index df8fbae..1ca9132 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "real-debrid-downloader", - "version": "1.4.10", + "version": "1.4.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "real-debrid-downloader", - "version": "1.4.10", + "version": "1.4.11", "license": "MIT", "dependencies": { "adm-zip": "^0.5.16", diff --git a/package.json b/package.json index 54a2264..4f663ae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.4.10", + "version": "1.4.11", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/extractor.ts b/src/main/extractor.ts index 1d6f400..725a731 100644 --- a/src/main/extractor.ts +++ b/src/main/extractor.ts @@ -36,6 +36,9 @@ export interface ExtractProgressUpdate { const MAX_EXTRACT_OUTPUT_BUFFER = 48 * 1024; const EXTRACT_PROGRESS_FILE = ".rd_extract_progress.json"; +const EXTRACT_BASE_TIMEOUT_MS = 6 * 60 * 1000; +const EXTRACT_PER_GIB_TIMEOUT_MS = 7 * 60 * 1000; +const EXTRACT_MAX_TIMEOUT_MS = 40 * 60 * 1000; type ExtractResumeState = { completedArchives: string[]; @@ -110,6 +113,17 @@ function shouldPreferExternalZip(archivePath: string): boolean { } } +function computeExtractTimeoutMs(archivePath: string): number { + try { + const stat = fs.statSync(archivePath); + const gib = stat.size / (1024 * 1024 * 1024); + const dynamicMs = EXTRACT_BASE_TIMEOUT_MS + Math.floor(gib * EXTRACT_PER_GIB_TIMEOUT_MS); + return Math.max(EXTRACT_BASE_TIMEOUT_MS, Math.min(EXTRACT_MAX_TIMEOUT_MS, dynamicMs)); + } catch { + return EXTRACT_BASE_TIMEOUT_MS; + } +} + function extractProgressFilePath(packageDir: string): string { return path.join(packageDir, EXTRACT_PROGRESS_FILE); } @@ -215,6 +229,7 @@ type ExtractSpawnResult = { ok: boolean; missingCommand: boolean; aborted: boolean; + timedOut: boolean; errorText: string; }; @@ -222,28 +237,51 @@ function runExtractCommand( command: string, args: string[], onChunk?: (chunk: string) => void, - signal?: AbortSignal + signal?: AbortSignal, + timeoutMs?: number ): Promise { if (signal?.aborted) { - return Promise.resolve({ ok: false, missingCommand: false, aborted: true, errorText: "aborted:extract" }); + return Promise.resolve({ ok: false, missingCommand: false, aborted: true, timedOut: false, errorText: "aborted:extract" }); } return new Promise((resolve) => { let settled = false; let output = ""; const child = spawn(command, args, { windowsHide: true }); + let timeoutId: NodeJS.Timeout | null = null; const finish = (result: ExtractSpawnResult): void => { if (settled) { return; } settled = true; + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } if (signal && onAbort) { signal.removeEventListener("abort", onAbort); } resolve(result); }; + if (timeoutMs && timeoutMs > 0) { + timeoutId = setTimeout(() => { + try { + child.kill(); + } catch { + // ignore + } + finish({ + ok: false, + missingCommand: false, + aborted: false, + timedOut: true, + errorText: `Entpacken Timeout nach ${Math.ceil(timeoutMs / 1000)}s` + }); + }, timeoutMs); + } + const onAbort = signal ? (): void => { try { @@ -251,7 +289,7 @@ function runExtractCommand( } catch { // ignore } - finish({ ok: false, missingCommand: false, aborted: true, errorText: "aborted:extract" }); + finish({ ok: false, missingCommand: false, aborted: true, timedOut: false, errorText: "aborted:extract" }); } : null; if (signal && onAbort) { @@ -275,13 +313,14 @@ function runExtractCommand( ok: false, missingCommand: text.toLowerCase().includes("enoent"), aborted: false, + timedOut: false, errorText: text }); }); child.on("close", (code) => { if (code === 0 || code === 1) { - finish({ ok: true, missingCommand: false, aborted: false, errorText: "" }); + finish({ ok: true, missingCommand: false, aborted: false, timedOut: false, errorText: "" }); return; } const cleaned = cleanErrorText(output); @@ -289,6 +328,7 @@ function runExtractCommand( ok: false, missingCommand: false, aborted: false, + timedOut: false, errorText: cleaned || `Exit Code ${String(code ?? "?")}` }); }); @@ -353,6 +393,7 @@ async function runExternalExtract( const command = await resolveExtractorCommand(); const passwords = passwordCandidates; let lastError = ""; + const timeoutMs = computeExtractTimeoutMs(archivePath); fs.mkdirSync(targetDir, { recursive: true }); @@ -375,7 +416,7 @@ async function runExternalExtract( } bestPercent = parsed; onArchiveProgress?.(bestPercent); - }, signal); + }, signal, timeoutMs); if (result.ok) { onArchiveProgress?.(100); return password; @@ -385,6 +426,11 @@ async function runExternalExtract( throw new Error("aborted:extract"); } + if (result.timedOut) { + lastError = result.errorText; + break; + } + if (result.missingCommand) { resolvedExtractorCommand = null; resolveFailureReason = NO_EXTRACTOR_MESSAGE; diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index db7a4dc..00b4198 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -205,22 +205,51 @@ export function App(): ReactElement { }, []); const downloadsTabActive = tab === "downloads"; + const deferredDownloadSearch = useDeferredValue(downloadSearch); + const downloadSearchQuery = deferredDownloadSearch.trim().toLowerCase(); + const downloadSearchActive = downloadSearchQuery.length > 0; + const totalPackageCount = snapshot.session.packageOrder.length; + const shouldLimitPackageRendering = downloadsTabActive + && snapshot.session.running + && !downloadSearchActive + && totalPackageCount > AUTO_RENDER_PACKAGE_LIMIT + && !showAllPackages; - const packages = useMemo(() => { + const packageIdsForView = useMemo(() => { if (!downloadsTabActive) { - return [] as PackageEntry[]; + return [] as string[]; } - return snapshot.session.packageOrder - .map((id: string) => snapshot.session.packages[id]) - .filter(Boolean); - }, [downloadsTabActive, snapshot.session.packageOrder, snapshot.session.packages]); + if (downloadSearchActive) { + return snapshot.session.packageOrder; + } + if (shouldLimitPackageRendering) { + return snapshot.session.packageOrder.slice(0, AUTO_RENDER_PACKAGE_LIMIT); + } + return snapshot.session.packageOrder; + }, [downloadsTabActive, downloadSearchActive, shouldLimitPackageRendering, snapshot.session.packageOrder]); const packageOrderKey = useMemo(() => { if (!downloadsTabActive) { return ""; } - return snapshot.session.packageOrder.join("|"); - }, [downloadsTabActive, snapshot.session.packageOrder]); + return packageIdsForView.join("|"); + }, [downloadsTabActive, packageIdsForView]); + + const packages = useMemo(() => { + if (!downloadsTabActive) { + return [] as PackageEntry[]; + } + + if (downloadSearchActive) { + return snapshot.session.packageOrder + .map((id: string) => snapshot.session.packages[id]) + .filter((pkg): pkg is PackageEntry => Boolean(pkg) && pkg.name.toLowerCase().includes(downloadSearchQuery)); + } + + return packageIdsForView + .map((id) => snapshot.session.packages[id]) + .filter((pkg): pkg is PackageEntry => Boolean(pkg)); + }, [downloadsTabActive, downloadSearchActive, downloadSearchQuery, packageIdsForView, snapshot.session.packageOrder, snapshot.session.packages]); const packagePosition = useMemo(() => { if (!downloadsTabActive) { @@ -238,18 +267,14 @@ export function App(): ReactElement { return new Map(); } const map = new Map(); - for (const packageId of snapshot.session.packageOrder) { - const pkg = snapshot.session.packages[packageId]; - if (!pkg) { - continue; - } + for (const pkg of packages) { const items = pkg.itemIds .map((id) => snapshot.session.items[id]) .filter(Boolean) as DownloadItem[]; - map.set(packageId, items); + map.set(pkg.id, items); } return map; - }, [downloadsTabActive, packageOrderKey, snapshot.session.items, snapshot.session.packages, snapshot.session.packageOrder]); + }, [downloadsTabActive, packageOrderKey, packages, snapshot.session.items]); useEffect(() => { if (!downloadsTabActive) { @@ -257,38 +282,18 @@ export function App(): ReactElement { } setCollapsedPackages((prev) => { const next: Record = {}; - const defaultCollapsed = snapshot.session.packageOrder.length >= 24; - for (const packageId of snapshot.session.packageOrder) { + const defaultCollapsed = totalPackageCount >= 24; + for (const packageId of packageIdsForView) { next[packageId] = prev[packageId] ?? defaultCollapsed; } return next; }); - }, [downloadsTabActive, packageOrderKey, snapshot.session.packageOrder.length]); + }, [downloadsTabActive, packageOrderKey, totalPackageCount, packageIdsForView]); - const deferredDownloadSearch = useDeferredValue(downloadSearch); - - const filteredPackages = useMemo(() => { - const query = deferredDownloadSearch.trim().toLowerCase(); - if (!query) { - return packages; - } - return packages.filter((pkg) => pkg.name.toLowerCase().includes(query)); - }, [packages, deferredDownloadSearch]); - - const downloadSearchActive = deferredDownloadSearch.trim().length > 0; - const shouldLimitPackageRendering = snapshot.session.running - && !downloadSearchActive - && filteredPackages.length > AUTO_RENDER_PACKAGE_LIMIT - && !showAllPackages; - - const visiblePackages = useMemo(() => { - if (!shouldLimitPackageRendering) { - return filteredPackages; - } - return filteredPackages.slice(0, AUTO_RENDER_PACKAGE_LIMIT); - }, [filteredPackages, shouldLimitPackageRendering]); - - const hiddenPackageCount = filteredPackages.length - visiblePackages.length; + const hiddenPackageCount = shouldLimitPackageRendering + ? Math.max(0, totalPackageCount - packages.length) + : 0; + const visiblePackages = packages; useEffect(() => { if (!snapshot.session.running) { @@ -855,8 +860,8 @@ export function App(): ReactElement { Dateien: {snapshot.stats.totalFiles} fertig Gesamt: {humanSize(snapshot.stats.totalDownloaded)} - {packages.length === 0 &&
Noch keine Pakete in der Queue.
} - {packages.length > 0 && filteredPackages.length === 0 &&
Keine Pakete passend zur Suche.
} + {totalPackageCount === 0 &&
Noch keine Pakete in der Queue.
} + {totalPackageCount > 0 && packages.length === 0 &&
Keine Pakete passend zur Suche.
} {hiddenPackageCount > 0 && (
Performance-Modus aktiv: {hiddenPackageCount} Paket(e) sind temporar ausgeblendet.