From 97c5bfaa7d071f7c946f4157e79deaba42a7e8b0 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Wed, 4 Mar 2026 14:23:29 +0100 Subject: [PATCH] Release v1.6.8 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix "Fertig" status on completed items: session recovery no longer resets "Entpacken - Ausstehend" to "Fertig (size)" — respects autoExtract setting - Extraction continues during pause instead of being aborted - Hybrid extraction recovery on start/resume: triggerPendingExtractions and recoverPostProcessingOnStartup now handle partial packages with hybridExtract - Move Up/Down buttons: optimistic UI update so packages move instantly Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- src/main/download-manager.ts | 84 +++++++++++++++++++++++++++--------- src/renderer/App.tsx | 11 +++++ 3 files changed, 76 insertions(+), 21 deletions(-) diff --git a/package.json b/package.json index 2cdd33f..0633cb2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.6.7", + "version": "1.6.8", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 342a06c..1f46d80 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -3042,9 +3042,9 @@ export class DownloadManager extends EventEmitter { const wasPaused = this.session.paused; this.session.paused = !this.session.paused; - // When pausing: abort active extractions so they don't continue during pause if (!wasPaused && this.session.paused) { - this.abortPostProcessing("pause"); + // Do NOT abort extraction on pause — extraction works on already-downloaded + // files and should continue while downloads are paused. this.speedEvents = []; this.speedBytesLastWindow = 0; this.speedBytesPerPackage.clear(); @@ -3129,8 +3129,13 @@ export class DownloadManager extends EventEmitter { } if (item.status === "completed") { const statusText = (item.fullStatus || "").trim(); - if (statusText && !isExtractedLabel(statusText) && !/^Fertig\b/i.test(statusText)) { - item.fullStatus = `Fertig (${humanSize(item.downloadedBytes)})`; + // Preserve extraction-related statuses (Ausstehend, Warten auf Parts, etc.) + if (/^Entpacken\b/i.test(statusText) || isExtractedLabel(statusText) || /^Fertig\b/i.test(statusText)) { + // keep as-is + } else if (statusText) { + item.fullStatus = this.settings.autoExtract + ? "Entpacken - Ausstehend" + : `Fertig (${humanSize(item.downloadedBytes)})`; } } } @@ -3601,7 +3606,27 @@ export class DownloadManager extends EventEmitter { const success = items.filter((item) => item.status === "completed").length; const failed = items.filter((item) => item.status === "failed").length; const cancelled = items.filter((item) => item.status === "cancelled").length; - if (success + failed + cancelled < items.length) { + const allDone = success + failed + cancelled >= items.length; + + // Hybrid extraction recovery: not all items done, but some completed + // with pending extraction status → re-label and trigger post-processing + // so extraction picks up where it left off. + if (!allDone && this.settings.autoExtract && this.settings.hybridExtract && success > 0 && failed === 0) { + const needsExtraction = items.some((item) => item.status === "completed" && !isExtractedLabel(item.fullStatus)); + if (needsExtraction) { + for (const item of items) { + if (item.status === "completed" && !isExtractedLabel(item.fullStatus)) { + item.fullStatus = "Entpacken - Ausstehend"; + item.updatedAt = nowMs(); + } + } + changed = true; + // Don't trigger extraction here — it will be triggered when the + // session starts via triggerPendingExtractions or item completions. + } + } + + if (!allDone) { continue; } @@ -3663,25 +3688,44 @@ export class DownloadManager extends EventEmitter { const success = items.filter((item) => item.status === "completed").length; const failed = items.filter((item) => item.status === "failed").length; const cancelled = items.filter((item) => item.status === "cancelled").length; - if (success + failed + cancelled < items.length || failed > 0 || cancelled > 0 || success === 0) { + const allDone = success + failed + cancelled >= items.length; + + // Full extraction: all items done, no failures + if (allDone && failed === 0 && cancelled === 0 && success > 0) { + const needsExtraction = items.some((item) => + item.status === "completed" && !isExtractedLabel(item.fullStatus) + ); + if (needsExtraction) { + pkg.status = "queued"; + pkg.updatedAt = nowMs(); + for (const item of items) { + if (item.status === "completed" && !isExtractedLabel(item.fullStatus)) { + item.fullStatus = "Entpacken - Ausstehend"; + item.updatedAt = nowMs(); + } + } + logger.info(`Entpacken via Start ausgelöst: pkg=${pkg.name}`); + void this.runPackagePostProcessing(packageId).catch((err) => logger.warn(`runPackagePostProcessing Fehler (triggerPending): ${compactErrorText(err)}`)); + } continue; } - const needsExtraction = items.some((item) => - item.status === "completed" && !isExtractedLabel(item.fullStatus) - ); - if (!needsExtraction) { - continue; - } - pkg.status = "queued"; - pkg.updatedAt = nowMs(); - for (const item of items) { - if (item.status === "completed" && !isExtractedLabel(item.fullStatus)) { - item.fullStatus = "Entpacken - Ausstehend"; - item.updatedAt = nowMs(); + + // Hybrid extraction: not all items done, but some completed and no failures + if (!allDone && this.settings.hybridExtract && success > 0 && failed === 0) { + const needsExtraction = items.some((item) => + item.status === "completed" && !isExtractedLabel(item.fullStatus) + ); + if (needsExtraction) { + for (const item of items) { + if (item.status === "completed" && !isExtractedLabel(item.fullStatus)) { + item.fullStatus = "Entpacken - Ausstehend"; + item.updatedAt = nowMs(); + } + } + logger.info(`Hybrid-Entpacken via Start ausgelöst: pkg=${pkg.name}, completed=${success}/${items.length}`); + void this.runPackagePostProcessing(packageId).catch((err) => logger.warn(`runPackagePostProcessing Fehler (triggerPendingHybrid): ${compactErrorText(err)}`)); } } - logger.info(`Entpacken via Start ausgelöst: pkg=${pkg.name}`); - void this.runPackagePostProcessing(packageId).catch((err) => logger.warn(`runPackagePostProcessing Fehler (triggerPending): ${compactErrorText(err)}`)); } } diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 5a60469..f7d60fb 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1498,10 +1498,21 @@ export function App(): ReactElement { pendingPackageOrderRef.current = [...order]; pendingPackageOrderAtRef.current = Date.now(); packageOrderRef.current = [...order]; + // Optimistic UI update — apply the new order immediately so the user + // sees the change without waiting for the backend round-trip. + setSnapshot((prev) => { + if (!prev) return prev; + return { ...prev, session: { ...prev.session, packageOrder: [...order] } }; + }); void window.rd.reorderPackages(order).catch((error) => { pendingPackageOrderRef.current = null; pendingPackageOrderAtRef.current = 0; packageOrderRef.current = serverPackageOrderRef.current; + // Rollback: restore original order from server + setSnapshot((prev) => { + if (!prev) return prev; + return { ...prev, session: { ...prev.session, packageOrder: serverPackageOrderRef.current } }; + }); showToast(`Sortierung fehlgeschlagen: ${String(error)}`, 2400); }); }, [selectedIds, snapshot.session.packages, showToast]);