Release v1.6.8

- 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 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-03-04 14:23:29 +01:00
parent 8d0c110415
commit 97c5bfaa7d
3 changed files with 76 additions and 21 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.6.7", "version": "1.6.8",
"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",

View File

@ -3042,9 +3042,9 @@ export class DownloadManager extends EventEmitter {
const wasPaused = this.session.paused; const wasPaused = this.session.paused;
this.session.paused = !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) { 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.speedEvents = [];
this.speedBytesLastWindow = 0; this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear(); this.speedBytesPerPackage.clear();
@ -3129,8 +3129,13 @@ export class DownloadManager extends EventEmitter {
} }
if (item.status === "completed") { if (item.status === "completed") {
const statusText = (item.fullStatus || "").trim(); const statusText = (item.fullStatus || "").trim();
if (statusText && !isExtractedLabel(statusText) && !/^Fertig\b/i.test(statusText)) { // Preserve extraction-related statuses (Ausstehend, Warten auf Parts, etc.)
item.fullStatus = `Fertig (${humanSize(item.downloadedBytes)})`; 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 success = items.filter((item) => item.status === "completed").length;
const failed = items.filter((item) => item.status === "failed").length; const failed = items.filter((item) => item.status === "failed").length;
const cancelled = items.filter((item) => item.status === "cancelled").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; continue;
} }
@ -3663,15 +3688,14 @@ export class DownloadManager extends EventEmitter {
const success = items.filter((item) => item.status === "completed").length; const success = items.filter((item) => item.status === "completed").length;
const failed = items.filter((item) => item.status === "failed").length; const failed = items.filter((item) => item.status === "failed").length;
const cancelled = items.filter((item) => item.status === "cancelled").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;
continue;
} // Full extraction: all items done, no failures
if (allDone && failed === 0 && cancelled === 0 && success > 0) {
const needsExtraction = items.some((item) => const needsExtraction = items.some((item) =>
item.status === "completed" && !isExtractedLabel(item.fullStatus) item.status === "completed" && !isExtractedLabel(item.fullStatus)
); );
if (!needsExtraction) { if (needsExtraction) {
continue;
}
pkg.status = "queued"; pkg.status = "queued";
pkg.updatedAt = nowMs(); pkg.updatedAt = nowMs();
for (const item of items) { for (const item of items) {
@ -3683,6 +3707,26 @@ export class DownloadManager extends EventEmitter {
logger.info(`Entpacken via Start ausgelöst: pkg=${pkg.name}`); logger.info(`Entpacken via Start ausgelöst: pkg=${pkg.name}`);
void this.runPackagePostProcessing(packageId).catch((err) => logger.warn(`runPackagePostProcessing Fehler (triggerPending): ${compactErrorText(err)}`)); void this.runPackagePostProcessing(packageId).catch((err) => logger.warn(`runPackagePostProcessing Fehler (triggerPending): ${compactErrorText(err)}`));
} }
continue;
}
// 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)}`));
}
}
}
} }
public retryExtraction(packageId: string): void { public retryExtraction(packageId: string): void {

View File

@ -1498,10 +1498,21 @@ export function App(): ReactElement {
pendingPackageOrderRef.current = [...order]; pendingPackageOrderRef.current = [...order];
pendingPackageOrderAtRef.current = Date.now(); pendingPackageOrderAtRef.current = Date.now();
packageOrderRef.current = [...order]; 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) => { void window.rd.reorderPackages(order).catch((error) => {
pendingPackageOrderRef.current = null; pendingPackageOrderRef.current = null;
pendingPackageOrderAtRef.current = 0; pendingPackageOrderAtRef.current = 0;
packageOrderRef.current = serverPackageOrderRef.current; 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); showToast(`Sortierung fehlgeschlagen: ${String(error)}`, 2400);
}); });
}, [selectedIds, snapshot.session.packages, showToast]); }, [selectedIds, snapshot.session.packages, showToast]);