From 92101e249a11a7825917af048806a81cb0a9f4ea Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Wed, 4 Mar 2026 02:57:33 +0100 Subject: [PATCH] Release v1.5.89 Co-Authored-By: Claude Opus 4.6 --- package.json | 2 +- src/main/app-controller.ts | 16 ++++++ src/main/download-manager.ts | 95 ++++++++++++++++++++++++++++++++++++ src/main/main.ts | 22 ++++++++- src/preload/preload.ts | 4 +- src/renderer/App.tsx | 7 ++- src/shared/ipc.ts | 4 +- src/shared/preload-api.ts | 4 +- 8 files changed, 147 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 55d1721..d94a06d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.5.88", + "version": "1.5.89", "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 318763d..c345e01 100644 --- a/src/main/app-controller.ts +++ b/src/main/app-controller.ts @@ -210,6 +210,10 @@ export class AppController { await this.manager.startPackages(packageIds); } + public async startItems(itemIds: string[]): Promise { + await this.manager.startItems(itemIds); + } + public stop(): void { this.manager.stop(); } @@ -308,6 +312,18 @@ export class AppController { clearHistory(this.storagePaths); } + public setPackagePriority(packageId: string, priority: string): void { + this.manager.setPackagePriority(packageId, priority as any); + } + + public skipItems(itemIds: string[]): void { + this.manager.skipItems(itemIds); + } + + public resetItems(itemIds: string[]): void { + this.manager.resetItems(itemIds); + } + public removeHistoryEntry(entryId: string): void { removeHistoryEntry(this.storagePaths, entryId); } diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 1e17d24..0548ccd 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -2627,6 +2627,101 @@ export class DownloadManager extends EventEmitter { }); } + public async startItems(itemIds: string[]): Promise { + const targetSet = new Set(itemIds); + + // Collect affected package IDs + const affectedPackageIds = new Set(); + for (const itemId of targetSet) { + const item = this.session.items[itemId]; + if (item) affectedPackageIds.add(item.packageId); + } + + // Enable affected packages if disabled + for (const pkgId of affectedPackageIds) { + const pkg = this.session.packages[pkgId]; + if (pkg && !pkg.enabled) { + pkg.enabled = true; + } + } + + // Recover stopped items + for (const itemId of targetSet) { + const item = this.session.items[itemId]; + if (!item) continue; + if (item.status === "cancelled" && item.fullStatus === "Gestoppt") { + const pkg = this.session.packages[item.packageId]; + if (pkg && !pkg.cancelled && pkg.enabled) { + item.status = "queued"; + item.fullStatus = "Wartet"; + item.lastError = ""; + item.speedBps = 0; + item.updatedAt = nowMs(); + } + } + } + + // If already running, add items to scheduler + if (this.session.running) { + for (const itemId of targetSet) { + const item = this.session.items[itemId]; + if (!item) continue; + if (item.status === "queued" || item.status === "reconnect_wait") { + this.runItemIds.add(item.id); + this.runPackageIds.add(item.packageId); + } + } + this.persistSoon(); + this.emitState(true); + return; + } + + // Not running: start with only specified items + const runItems = [...targetSet] + .map((id) => this.session.items[id]) + .filter((item) => { + if (!item) return false; + if (item.status !== "queued" && item.status !== "reconnect_wait") return false; + const pkg = this.session.packages[item.packageId]; + return Boolean(pkg && !pkg.cancelled && pkg.enabled); + }); + if (runItems.length === 0) { + this.persistSoon(); + this.emitState(true); + return; + } + this.runItemIds = new Set(runItems.map((item) => item.id)); + this.runPackageIds = new Set(runItems.map((item) => item.packageId)); + this.runOutcomes.clear(); + this.runCompletedPackages.clear(); + this.retryAfterByItem.clear(); + this.session.running = true; + this.session.paused = false; + this.session.runStartedAt = nowMs(); + this.session.totalDownloadedBytes = 0; + this.session.summaryText = ""; + this.session.reconnectUntil = 0; + this.session.reconnectReason = ""; + this.speedEvents = []; + this.speedBytesLastWindow = 0; + this.speedBytesPerPackage.clear(); + this.speedEventsHead = 0; + this.lastGlobalProgressBytes = 0; + this.lastGlobalProgressAt = nowMs(); + this.summary = null; + this.nonResumableActive = 0; + this.persistSoon(); + this.emitState(true); + logger.info(`Start (nur Items: ${itemIds.length}): ${runItems.length} Items`); + void this.ensureScheduler().catch((error) => { + logger.error(`Scheduler abgestürzt: ${compactErrorText(error)}`); + this.session.running = false; + this.session.paused = false; + this.persistSoon(); + this.emitState(true); + }); + } + public async start(): Promise { if (this.session.running) { return; diff --git a/src/main/main.ts b/src/main/main.ts index c4d3686..f17fb24 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -292,6 +292,10 @@ function registerIpcHandlers(): void { if (!Array.isArray(packageIds)) throw new Error("packageIds muss ein Array sein"); return controller.startPackages(packageIds); }); + ipcMain.handle(IPC_CHANNELS.START_ITEMS, (_event: IpcMainInvokeEvent, itemIds: string[]) => { + if (!Array.isArray(itemIds)) throw new Error("itemIds muss ein Array sein"); + return controller.startItems(itemIds); + }); ipcMain.handle(IPC_CHANNELS.STOP, () => controller.stop()); ipcMain.handle(IPC_CHANNELS.TOGGLE_PAUSE, () => controller.togglePause()); ipcMain.handle(IPC_CHANNELS.CANCEL_PACKAGE, (_event: IpcMainInvokeEvent, packageId: string) => { @@ -339,13 +343,29 @@ function registerIpcHandlers(): void { if (!Array.isArray(itemIds)) throw new Error("itemIds must be an array"); 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); + }); ipcMain.handle(IPC_CHANNELS.GET_HISTORY, () => controller.getHistory()); ipcMain.handle(IPC_CHANNELS.CLEAR_HISTORY, () => controller.clearHistory()); ipcMain.handle(IPC_CHANNELS.REMOVE_HISTORY_ENTRY, (_event: IpcMainInvokeEvent, entryId: string) => { validateString(entryId, "entryId"); return controller.removeHistoryEntry(entryId); }); - ipcMain.handle(IPC_CHANNELS.EXPORT_QUEUE, () => controller.exportQueue()); + ipcMain.handle(IPC_CHANNELS.EXPORT_QUEUE, async () => { + const options = { + defaultPath: `rd-queue-export.json`, + filters: [{ name: "Queue Export", extensions: ["json"] }] + }; + const result = mainWindow ? await dialog.showSaveDialog(mainWindow, options) : await dialog.showSaveDialog(options); + if (result.canceled || !result.filePath) { + return { saved: false }; + } + const json = controller.exportQueue(); + await fs.promises.writeFile(result.filePath, json, "utf8"); + return { saved: true }; + }); ipcMain.handle(IPC_CHANNELS.IMPORT_QUEUE, (_event: IpcMainInvokeEvent, json: string) => { validateString(json, "json"); const bytes = Buffer.byteLength(json, "utf8"); diff --git a/src/preload/preload.ts b/src/preload/preload.ts index 259141d..cc26e32 100644 --- a/src/preload/preload.ts +++ b/src/preload/preload.ts @@ -38,7 +38,7 @@ const api: ElectronApi = { reorderPackages: (packageIds: string[]): Promise => ipcRenderer.invoke(IPC_CHANNELS.REORDER_PACKAGES, packageIds), removeItem: (itemId: string): Promise => ipcRenderer.invoke(IPC_CHANNELS.REMOVE_ITEM, itemId), togglePackage: (packageId: string): Promise => ipcRenderer.invoke(IPC_CHANNELS.TOGGLE_PACKAGE, packageId), - exportQueue: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.EXPORT_QUEUE), + exportQueue: (): Promise<{ saved: boolean }> => ipcRenderer.invoke(IPC_CHANNELS.EXPORT_QUEUE), importQueue: (json: string): Promise<{ addedPackages: number; addedLinks: number }> => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_QUEUE, json), toggleClipboard: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.TOGGLE_CLIPBOARD), pickFolder: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.PICK_FOLDER), @@ -58,6 +58,8 @@ const api: ElectronApi = { removeHistoryEntry: (entryId: string): Promise => ipcRenderer.invoke(IPC_CHANNELS.REMOVE_HISTORY_ENTRY, entryId), setPackagePriority: (packageId: string, priority: string): Promise => ipcRenderer.invoke(IPC_CHANNELS.SET_PACKAGE_PRIORITY, packageId, priority), skipItems: (itemIds: string[]): Promise => ipcRenderer.invoke(IPC_CHANNELS.SKIP_ITEMS, itemIds), + resetItems: (itemIds: string[]): Promise => ipcRenderer.invoke(IPC_CHANNELS.RESET_ITEMS, itemIds), + startItems: (itemIds: string[]): Promise => ipcRenderer.invoke(IPC_CHANNELS.START_ITEMS, itemIds), onStateUpdate: (callback: (snapshot: UiSnapshot) => void): (() => void) => { const listener = (_event: unknown, snapshot: UiSnapshot): void => callback(snapshot); ipcRenderer.on(IPC_CHANNELS.STATE_UPDATE, listener); diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 69f3771..3c3f0df 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -2833,10 +2833,13 @@ export function App(): ReactElement { const hasItems = [...selectedIds].some((id) => snapshot.session.items[id]); return (
e.stopPropagation()}> - {(!contextMenu.itemId || multi) && hasPackages && ( + {(hasPackages || hasItems) && ( )} diff --git a/src/shared/ipc.ts b/src/shared/ipc.ts index a352555..db0c47d 100644 --- a/src/shared/ipc.ts +++ b/src/shared/ipc.ts @@ -41,5 +41,7 @@ export const IPC_CHANNELS = { CLEAR_HISTORY: "history:clear", REMOVE_HISTORY_ENTRY: "history:remove-entry", SET_PACKAGE_PRIORITY: "queue:set-package-priority", - SKIP_ITEMS: "queue:skip-items" + SKIP_ITEMS: "queue:skip-items", + RESET_ITEMS: "queue:reset-items", + START_ITEMS: "queue:start-items" } as const; diff --git a/src/shared/preload-api.ts b/src/shared/preload-api.ts index 05af6cd..f0b360a 100644 --- a/src/shared/preload-api.ts +++ b/src/shared/preload-api.ts @@ -34,7 +34,7 @@ export interface ElectronApi { reorderPackages: (packageIds: string[]) => Promise; removeItem: (itemId: string) => Promise; togglePackage: (packageId: string) => Promise; - exportQueue: () => Promise; + exportQueue: () => Promise<{ saved: boolean }>; importQueue: (json: string) => Promise<{ addedPackages: number; addedLinks: number }>; toggleClipboard: () => Promise; pickFolder: () => Promise; @@ -54,6 +54,8 @@ export interface ElectronApi { removeHistoryEntry: (entryId: string) => Promise; setPackagePriority: (packageId: string, priority: PackagePriority) => Promise; skipItems: (itemIds: string[]) => Promise; + resetItems: (itemIds: string[]) => Promise; + startItems: (itemIds: string[]) => Promise; onStateUpdate: (callback: (snapshot: UiSnapshot) => void) => () => void; onClipboardDetected: (callback: (links: string[]) => void) => () => void; onUpdateInstallProgress: (callback: (progress: UpdateInstallProgress) => void) => () => void;