From b02aef2af949caf8ac2a5e77a937001d8db822ac Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Wed, 4 Mar 2026 16:50:59 +0100 Subject: [PATCH] Release v1.6.16 Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- src/main/app-controller.ts | 14 ++++++++++++-- src/main/download-manager.ts | 18 ++++++++++++++---- src/main/main.ts | 21 +++++++++++++-------- src/renderer/App.tsx | 8 ++++---- 5 files changed, 44 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index f335816..616b6cc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.6.15", + "version": "1.6.16", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/app-controller.ts b/src/main/app-controller.ts index 5a8869b..6a67726 100644 --- a/src/main/app-controller.ts +++ b/src/main/app-controller.ts @@ -81,8 +81,15 @@ export class AppController { void this.manager.getStartConflicts().then((conflicts) => { const hasConflicts = conflicts.length > 0; if (this.hasAnyProviderToken(this.settings) && !hasConflicts) { - this.autoResumePending = true; - logger.info("Auto-Resume beim Start vorgemerkt"); + // If the onState handler is already set (renderer connected), start immediately. + // Otherwise mark as pending so the onState setter triggers the start. + if (this.onStateHandler) { + logger.info("Auto-Resume beim Start aktiviert (nach Konflikt-Check)"); + void this.manager.start().catch((err) => logger.warn(`Auto-Resume Start Fehler: ${String(err)}`)); + } else { + this.autoResumePending = true; + logger.info("Auto-Resume beim Start vorgemerkt"); + } } else if (hasConflicts) { logger.info("Auto-Resume übersprungen: Start-Konflikte erkannt"); } @@ -299,6 +306,9 @@ export class AppController { // so no extraction tasks from it should keep running. this.manager.stop(); this.manager.abortAllPostProcessing(); + // Cancel any deferred persist timer so the old in-memory session + // does not overwrite the restored session file on disk. + this.manager.clearPersistTimer(); const restoredSession = parsed.session as ReturnType; saveSession(this.storagePaths, restoredSession); return { restored: true, message: "Backup wiederhergestellt. Bitte App neustarten." }; diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 76b702f..8bf3408 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -2527,6 +2527,7 @@ export class DownloadManager extends EventEmitter { pkg.cancelled = false; pkg.enabled = true; pkg.updatedAt = nowMs(); + this.historyRecordedPackages.delete(packageId); logger.info(`Paket "${pkg.name}" zurückgesetzt (${itemIds.length} Items)`); this.persistSoon(); @@ -3302,7 +3303,7 @@ export class DownloadManager extends EventEmitter { } } - private clearPersistTimer(): void { + public clearPersistTimer(): void { if (!this.persistTimer) { return; } @@ -3557,7 +3558,11 @@ export class DownloadManager extends EventEmitter { await new Promise((resolve) => { this.packagePostProcessWaiters.push({ packageId, resolve }); }); - this.packagePostProcessActive += 1; + // Guard: stop() may have reset the counter to 0 while we were waiting. + // Only increment if below max to avoid phantom slot usage. + if (this.packagePostProcessActive < maxConcurrent) { + this.packagePostProcessActive += 1; + } } private releasePostProcessSlot(): void { @@ -6715,9 +6720,14 @@ export class DownloadManager extends EventEmitter { }; this.session.summaryText = `Summary: Dauer ${duration}s, Ø Speed ${humanSize(avgSpeed)}/s, Erfolg ${success}/${total}`; this.runItemIds.clear(); - this.runPackageIds.clear(); this.runOutcomes.clear(); - this.runCompletedPackages.clear(); + // Keep runPackageIds and runCompletedPackages alive when post-processing tasks + // are still running (autoExtractWhenStopped) so handlePackagePostProcessing() + // can still update runCompletedPackages. They are cleared by the next start(). + if (this.packagePostProcessTasks.size === 0) { + this.runPackageIds.clear(); + this.runCompletedPackages.clear(); + } this.retryAfterByItem.clear(); this.retryStateByItem.clear(); this.reservedTargetPaths.clear(); diff --git a/src/main/main.ts b/src/main/main.ts index d123375..2049632 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -50,6 +50,7 @@ process.on("unhandledRejection", (reason) => { let mainWindow: BrowserWindow | null = null; let tray: Tray | null = null; let clipboardTimer: ReturnType | null = null; +let updateQuitTimer: ReturnType | null = null; let lastClipboardText = ""; const controller = new AppController(); const CLIPBOARD_MAX_TEXT_CHARS = 50_000; @@ -236,7 +237,7 @@ function registerIpcHandlers(): void { mainWindow.webContents.send(IPC_CHANNELS.UPDATE_INSTALL_PROGRESS, progress); }); if (result.started) { - setTimeout(() => { + updateQuitTimer = setTimeout(() => { app.quit(); }, 2500); } @@ -289,8 +290,8 @@ function registerIpcHandlers(): void { ipcMain.handle(IPC_CHANNELS.CLEAR_ALL, () => controller.clearAll()); ipcMain.handle(IPC_CHANNELS.START, () => controller.start()); ipcMain.handle(IPC_CHANNELS.START_PACKAGES, (_event: IpcMainInvokeEvent, packageIds: string[]) => { - if (!Array.isArray(packageIds)) throw new Error("packageIds muss ein Array sein"); - return controller.startPackages(packageIds); + validateStringArray(packageIds ?? [], "packageIds"); + return controller.startPackages(packageIds ?? []); }); ipcMain.handle(IPC_CHANNELS.START_ITEMS, (_event: IpcMainInvokeEvent, itemIds: string[]) => { validateStringArray(itemIds ?? [], "itemIds"); @@ -337,15 +338,18 @@ function registerIpcHandlers(): void { ipcMain.handle(IPC_CHANNELS.SET_PACKAGE_PRIORITY, (_event: IpcMainInvokeEvent, packageId: string, priority: string) => { validateString(packageId, "packageId"); validateString(priority, "priority"); - return controller.setPackagePriority(packageId, priority as any); + if (priority !== "high" && priority !== "normal" && priority !== "low") { + throw new Error("priority muss 'high', 'normal' oder 'low' sein"); + } + return controller.setPackagePriority(packageId, priority); }); ipcMain.handle(IPC_CHANNELS.SKIP_ITEMS, (_event: IpcMainInvokeEvent, itemIds: string[]) => { - if (!Array.isArray(itemIds)) throw new Error("itemIds must be an array"); - return controller.skipItems(itemIds); + validateStringArray(itemIds ?? [], "itemIds"); + return controller.skipItems(itemIds ?? []); }); ipcMain.handle(IPC_CHANNELS.RESET_ITEMS, (_event: IpcMainInvokeEvent, itemIds: string[]) => { - if (!Array.isArray(itemIds)) throw new Error("itemIds must be an array"); - return controller.resetItems(itemIds); + validateStringArray(itemIds ?? [], "itemIds"); + return controller.resetItems(itemIds ?? []); }); ipcMain.handle(IPC_CHANNELS.GET_HISTORY, () => controller.getHistory()); ipcMain.handle(IPC_CHANNELS.CLEAR_HISTORY, () => controller.clearHistory()); @@ -493,6 +497,7 @@ app.on("window-all-closed", () => { }); app.on("before-quit", () => { + if (updateQuitTimer) { clearTimeout(updateQuitTimer); updateQuitTimer = null; } stopClipboardWatcher(); destroyTray(); try { diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 7a3ec6b..4a8fdfb 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -2935,7 +2935,7 @@ export function App(): ReactElement { {(hasPackages || hasStartableItems) && (