From 7446e07a8ce8a4e8fe73d77f7bea493045dea641 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Wed, 4 Mar 2026 09:54:37 +0100 Subject: [PATCH] Release v1.6.4 Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- src/main/debrid.ts | 6 ++++ src/main/download-manager.ts | 59 +++++++++++++++++++++++++----------- src/main/extractor.ts | 5 +-- src/renderer/App.tsx | 29 ++++++++++++------ 5 files changed, 71 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index cdff46e..02aa6f2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.6.3", + "version": "1.6.4", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/debrid.ts b/src/main/debrid.ts index 3890fc2..09ff210 100644 --- a/src/main/debrid.ts +++ b/src/main/debrid.ts @@ -417,6 +417,7 @@ async function resolveRapidgatorFilename(link: string, signal?: AbortSignal): Pr signal: withTimeoutSignal(signal, API_TIMEOUT_MS) }); if (!response.ok) { + try { await response.body?.cancel(); } catch { /* drain socket */ } if (shouldRetryStatus(response.status) && attempt < REQUEST_RETRIES + 2) { await sleepWithSignal(retryDelayForResponse(response, attempt), signal); continue; @@ -432,9 +433,11 @@ async function resolveRapidgatorFilename(link: string, signal?: AbortSignal): Pr && !contentType.includes("text/plain") && !contentType.includes("text/xml") && !contentType.includes("application/xml")) { + try { await response.body?.cancel(); } catch { /* drain socket */ } return ""; } if (!contentType && Number.isFinite(contentLength) && contentLength > RAPIDGATOR_SCAN_MAX_BYTES) { + try { await response.body?.cancel(); } catch { /* drain socket */ } return ""; } @@ -548,10 +551,12 @@ export async function checkRapidgatorOnline( }); if (response.status === 404) { + try { await response.body?.cancel(); } catch { /* drain socket */ } return { online: false, fileName: "", fileSize: null }; } if (!response.ok) { + try { await response.body?.cancel(); } catch { /* drain socket */ } if (shouldRetryStatus(response.status) && attempt <= REQUEST_RETRIES) { await sleepWithSignal(retryDelayForResponse(response, attempt), signal); continue; @@ -561,6 +566,7 @@ export async function checkRapidgatorOnline( const finalUrl = response.url || link; if (!finalUrl.includes(fileId)) { + try { await response.body?.cancel(); } catch { /* drain socket */ } return { online: false, fileName: "", fileSize: null }; } diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index f8294e5..e22a235 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -1648,6 +1648,10 @@ export class DownloadManager extends EventEmitter { for (const item of Object.values(this.session.items)) { if (item.status !== "queued") continue; if (item.onlineStatus) continue; // already checked + try { + const host = new URL(item.url).hostname.toLowerCase(); + if (host !== "rapidgator.net" && !host.endsWith(".rapidgator.net") && host !== "rg.to" && !host.endsWith(".rg.to")) continue; + } catch { continue; } uncheckedIds.push(item.id); } if (uncheckedIds.length > 0) { @@ -2659,10 +2663,12 @@ export class DownloadManager extends EventEmitter { this.runOutcomes.clear(); this.runCompletedPackages.clear(); this.retryAfterByItem.clear(); + this.retryStateByItem.clear(); this.session.running = true; this.session.paused = false; this.session.runStartedAt = nowMs(); this.session.totalDownloadedBytes = 0; + this.sessionDownloadedBytes = 0; this.session.summaryText = ""; this.session.reconnectUntil = 0; this.session.reconnectReason = ""; @@ -2760,10 +2766,12 @@ export class DownloadManager extends EventEmitter { this.runOutcomes.clear(); this.runCompletedPackages.clear(); this.retryAfterByItem.clear(); + this.retryStateByItem.clear(); this.session.running = true; this.session.paused = false; this.session.runStartedAt = nowMs(); this.session.totalDownloadedBytes = 0; + this.sessionDownloadedBytes = 0; this.session.summaryText = ""; this.session.reconnectUntil = 0; this.session.reconnectReason = ""; @@ -2930,6 +2938,7 @@ export class DownloadManager extends EventEmitter { this.abortPostProcessing("stop"); for (const waiter of this.packagePostProcessWaiters) { waiter.resolve(); } this.packagePostProcessWaiters = []; + this.packagePostProcessActive = 0; for (const active of this.activeTasks.values()) { active.abortReason = "stop"; active.abortController.abort("stop"); @@ -3428,9 +3437,10 @@ export class DownloadManager extends EventEmitter { } } logger.error(`claimTargetPath: Limit erreicht für ${preferredPath}`); - this.reservedTargetPaths.set(pathKey(preferredPath), itemId); - this.claimedTargetPathByItem.set(itemId, preferredPath); - return preferredPath; + const fallbackPath = path.join(parsed.dir, `${parsed.name} (${Date.now()})${parsed.ext}`); + this.reservedTargetPaths.set(pathKey(fallbackPath), itemId); + this.claimedTargetPathByItem.set(itemId, fallbackPath); + return fallbackPath; } private releaseTargetPath(itemId: string): void { @@ -4433,6 +4443,7 @@ export class DownloadManager extends EventEmitter { // ignore } this.releaseTargetPath(item.id); + this.dropItemContribution(item.id); item.downloadedBytes = 0; item.progressPercent = 0; item.totalBytes = (item.totalBytes || 0) > 0 ? item.totalBytes : null; @@ -4594,10 +4605,12 @@ export class DownloadManager extends EventEmitter { const shouldFreshRetry = !active.freshRetryUsed && isFetchFailure(errorText); const isHttp416 = /(^|\D)416(\D|$)/.test(errorText); if (isHttp416) { - try { - fs.rmSync(item.targetPath, { force: true }); - } catch { - // ignore + if (claimedTargetPath) { + try { + fs.rmSync(claimedTargetPath, { force: true }); + } catch { + // ignore + } } this.releaseTargetPath(item.id); item.downloadedBytes = 0; @@ -4619,10 +4632,12 @@ export class DownloadManager extends EventEmitter { active.freshRetryUsed = true; item.retries += 1; logger.warn(`Netzwerkfehler: item=${item.fileName || item.id}, fresh retry, error=${errorText}, provider=${item.provider || "?"}`); - try { - fs.rmSync(item.targetPath, { force: true }); - } catch { - // ignore + if (claimedTargetPath) { + try { + fs.rmSync(claimedTargetPath, { force: true }); + } catch { + // ignore + } } this.releaseTargetPath(item.id); this.queueRetry(item, active, 300, "Netzwerkfehler erkannt, frischer Retry"); @@ -4854,6 +4869,7 @@ export class DownloadManager extends EventEmitter { } if (this.settings.autoReconnect && [429, 503].includes(response.status)) { this.requestReconnect(`HTTP ${response.status}`); + throw new Error(lastError); } if (attempt < maxAttempts) { item.retries += 1; @@ -5990,7 +6006,7 @@ export class DownloadManager extends EventEmitter { continue; } const status = entry.fullStatus || ""; - if (/^Entpacken\b/i.test(status) || /^Fertig\b/i.test(status)) { + if (/^Entpacken\b/i.test(status)) { if (result.extracted > 0 && result.failed === 0) { entry.fullStatus = formatExtractDone(nowMs() - hybridExtractStartMs); } else { @@ -6006,6 +6022,13 @@ export class DownloadManager extends EventEmitter { return; } logger.warn(`Hybrid-Extract Fehler: pkg=${pkg.name}, reason=${compactErrorText(error)}`); + const errorAt = nowMs(); + for (const entry of hybridItems) { + if (entry.fullStatus === "Entpacken - Ausstehend" || entry.fullStatus === "Entpacken - Warten auf Parts") { + entry.fullStatus = `Entpacken - Error`; + entry.updatedAt = errorAt; + } + } } } @@ -6383,11 +6406,11 @@ export class DownloadManager extends EventEmitter { return; } - const allCompleted = pkg.itemIds.every((itemId) => { + const allDone = pkg.itemIds.every((itemId) => { const item = this.session.items[itemId]; - return !item || item.status === "completed"; + return !item || item.status === "completed" || item.status === "cancelled" || item.status === "failed"; }); - if (!allCompleted) { + if (!allDone) { return; } @@ -6439,7 +6462,7 @@ export class DownloadManager extends EventEmitter { if (policy === "package_done") { const hasOpen = pkg.itemIds.some((id) => { const item = this.session.items[id]; - return item != null && item.status !== "completed"; + return item != null && item.status !== "completed" && item.status !== "cancelled" && item.status !== "failed"; }); if (!hasOpen) { // With autoExtract: only remove once ALL items are extracted, not just downloaded @@ -6550,9 +6573,9 @@ export class DownloadManager extends EventEmitter { completedDownloads += 1; } else if (item.status === "failed") { failedDownloads += 1; - } else if (item.status === "downloading" || item.status === "validating") { + } else if (item.status === "downloading" || item.status === "validating" || item.status === "integrity_check") { activeDownloads += 1; - } else if (item.status === "queued" || item.status === "reconnect_wait") { + } else if (item.status === "queued" || item.status === "reconnect_wait" || item.status === "paused") { queuedDownloads += 1; } } diff --git a/src/main/extractor.ts b/src/main/extractor.ts index c65b9ad..d477d49 100644 --- a/src/main/extractor.ts +++ b/src/main/extractor.ts @@ -1219,9 +1219,10 @@ async function resolveExtractorCommandInternal(): Promise { if (isAbsoluteCommand(command) && !fs.existsSync(command)) { continue; } - const probeArgs = command.toLowerCase().includes("winrar") ? ["-?"] : ["?"]; + const lower = command.toLowerCase(); + const probeArgs = (lower.includes("winrar") || lower.includes("unrar")) ? ["-?"] : ["?"]; const probe = await runExtractCommand(command, probeArgs, undefined, undefined, EXTRACTOR_PROBE_TIMEOUT_MS); - if (probe.ok) { + if (probe.ok || (!probe.missingCommand && !probe.timedOut)) { resolvedExtractorCommand = command; resolveFailureReason = ""; resolveFailureAt = 0; diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 9b61a4e..3048987 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -117,7 +117,7 @@ function formatHoster(item: DownloadItem): string { const hoster = extractHoster(item.url); const label = hoster || "-"; if (item.provider) { - return `${label} via. ${providerLabels[item.provider]}`; + return `${label} via ${providerLabels[item.provider]}`; } return label; } @@ -2010,20 +2010,23 @@ export function App(): ReactElement { value={settingsDraft.maxParallel} onChange={(e) => { const val = Math.max(1, Math.min(50, Number(e.target.value) || 1)); + settingsDirtyRef.current = true; setSettingsDraft((prev) => ({ ...prev, maxParallel: val })); - void window.rd.updateSettings({ maxParallel: val }); + void window.rd.updateSettings({ maxParallel: val }).finally(() => { settingsDirtyRef.current = false; }); }} />
@@ -2035,8 +2038,9 @@ export function App(): ReactElement { checked={settingsDraft.speedLimitEnabled} onChange={(e) => { const next = e.target.checked; + settingsDirtyRef.current = true; setSettingsDraft((prev) => ({ ...prev, speedLimitEnabled: next })); - void window.rd.updateSettings({ speedLimitEnabled: next }); + void window.rd.updateSettings({ speedLimitEnabled: next }).finally(() => { settingsDirtyRef.current = false; }); }} />
@@ -2054,8 +2058,9 @@ export function App(): ReactElement { return; } const kbps = Math.floor(parsed * 1024); + settingsDirtyRef.current = true; setSettingsDraft((prev) => ({ ...prev, speedLimitKbps: kbps })); - void window.rd.updateSettings({ speedLimitKbps: kbps }); + void window.rd.updateSettings({ speedLimitKbps: kbps }).finally(() => { settingsDirtyRef.current = false; }); setSpeedLimitInput(formatMbpsInputFromKbps(kbps)); }} /> @@ -2063,15 +2068,17 @@ export function App(): ReactElement {
@@ -2324,6 +2331,8 @@ export function App(): ReactElement { void Promise.all([...idSet].map(id => window.rd.removeHistoryEntry(id))).then(() => { setHistoryEntries((prev) => prev.filter((e) => !idSet.has(e.id))); setSelectedHistoryIds(new Set()); + }).catch(() => { + void window.rd.getHistory().then((entries) => { setHistoryEntries(entries); setSelectedHistoryIds(new Set()); }).catch(() => {}); }); }}>Ausgewählte entfernen ({selectedHistoryIds.size}) )} @@ -2992,6 +3001,8 @@ export function App(): ReactElement { void Promise.all(ids.map(id => window.rd.removeHistoryEntry(id))).then(() => { setHistoryEntries((prev) => prev.filter((e) => !selectedHistoryIds.has(e.id))); setSelectedHistoryIds(new Set()); + }).catch(() => { + void window.rd.getHistory().then((entries) => { setHistoryEntries(entries); setSelectedHistoryIds(new Set()); }).catch(() => {}); }); setHistoryCtxMenu(null); }; @@ -3216,7 +3227,7 @@ const PackageCard = memo(function PackageCard({ pkg, items, packageSpeed, isFirs
- {extracting &&
} + {useExtractSplit &&
}
{!collapsed && items.map((item) => (
{ e.stopPropagation(); onSelect(item.id, e.ctrlKey); }} onMouseDown={(e) => { e.stopPropagation(); onSelectMouseDown(item.id, e); }} onMouseEnter={() => onSelectMouseEnter(item.id)} onContextMenu={(e) => { e.preventDefault(); e.stopPropagation(); onContextMenu(pkg.id, item.id, e.clientX, e.clientY); }}>