Release v1.5.89

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-03-04 02:57:33 +01:00
parent a18ab484cc
commit 92101e249a
8 changed files with 147 additions and 7 deletions

View File

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

@ -210,6 +210,10 @@ export class AppController {
await this.manager.startPackages(packageIds); await this.manager.startPackages(packageIds);
} }
public async startItems(itemIds: string[]): Promise<void> {
await this.manager.startItems(itemIds);
}
public stop(): void { public stop(): void {
this.manager.stop(); this.manager.stop();
} }
@ -308,6 +312,18 @@ export class AppController {
clearHistory(this.storagePaths); 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 { public removeHistoryEntry(entryId: string): void {
removeHistoryEntry(this.storagePaths, entryId); removeHistoryEntry(this.storagePaths, entryId);
} }

View File

@ -2627,6 +2627,101 @@ export class DownloadManager extends EventEmitter {
}); });
} }
public async startItems(itemIds: string[]): Promise<void> {
const targetSet = new Set(itemIds);
// Collect affected package IDs
const affectedPackageIds = new Set<string>();
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<void> { public async start(): Promise<void> {
if (this.session.running) { if (this.session.running) {
return; return;

View File

@ -292,6 +292,10 @@ function registerIpcHandlers(): void {
if (!Array.isArray(packageIds)) throw new Error("packageIds muss ein Array sein"); if (!Array.isArray(packageIds)) throw new Error("packageIds muss ein Array sein");
return controller.startPackages(packageIds); 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.STOP, () => controller.stop());
ipcMain.handle(IPC_CHANNELS.TOGGLE_PAUSE, () => controller.togglePause()); ipcMain.handle(IPC_CHANNELS.TOGGLE_PAUSE, () => controller.togglePause());
ipcMain.handle(IPC_CHANNELS.CANCEL_PACKAGE, (_event: IpcMainInvokeEvent, packageId: string) => { 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"); if (!Array.isArray(itemIds)) throw new Error("itemIds must be an array");
return controller.skipItems(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);
});
ipcMain.handle(IPC_CHANNELS.GET_HISTORY, () => controller.getHistory()); ipcMain.handle(IPC_CHANNELS.GET_HISTORY, () => controller.getHistory());
ipcMain.handle(IPC_CHANNELS.CLEAR_HISTORY, () => controller.clearHistory()); ipcMain.handle(IPC_CHANNELS.CLEAR_HISTORY, () => controller.clearHistory());
ipcMain.handle(IPC_CHANNELS.REMOVE_HISTORY_ENTRY, (_event: IpcMainInvokeEvent, entryId: string) => { ipcMain.handle(IPC_CHANNELS.REMOVE_HISTORY_ENTRY, (_event: IpcMainInvokeEvent, entryId: string) => {
validateString(entryId, "entryId"); validateString(entryId, "entryId");
return controller.removeHistoryEntry(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) => { ipcMain.handle(IPC_CHANNELS.IMPORT_QUEUE, (_event: IpcMainInvokeEvent, json: string) => {
validateString(json, "json"); validateString(json, "json");
const bytes = Buffer.byteLength(json, "utf8"); const bytes = Buffer.byteLength(json, "utf8");

View File

@ -38,7 +38,7 @@ const api: ElectronApi = {
reorderPackages: (packageIds: string[]): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.REORDER_PACKAGES, packageIds), reorderPackages: (packageIds: string[]): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.REORDER_PACKAGES, packageIds),
removeItem: (itemId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.REMOVE_ITEM, itemId), removeItem: (itemId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.REMOVE_ITEM, itemId),
togglePackage: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.TOGGLE_PACKAGE, packageId), togglePackage: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.TOGGLE_PACKAGE, packageId),
exportQueue: (): Promise<string> => 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), importQueue: (json: string): Promise<{ addedPackages: number; addedLinks: number }> => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_QUEUE, json),
toggleClipboard: (): Promise<boolean> => ipcRenderer.invoke(IPC_CHANNELS.TOGGLE_CLIPBOARD), toggleClipboard: (): Promise<boolean> => ipcRenderer.invoke(IPC_CHANNELS.TOGGLE_CLIPBOARD),
pickFolder: (): Promise<string | null> => ipcRenderer.invoke(IPC_CHANNELS.PICK_FOLDER), pickFolder: (): Promise<string | null> => ipcRenderer.invoke(IPC_CHANNELS.PICK_FOLDER),
@ -58,6 +58,8 @@ const api: ElectronApi = {
removeHistoryEntry: (entryId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.REMOVE_HISTORY_ENTRY, entryId), removeHistoryEntry: (entryId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.REMOVE_HISTORY_ENTRY, entryId),
setPackagePriority: (packageId: string, priority: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.SET_PACKAGE_PRIORITY, packageId, priority), setPackagePriority: (packageId: string, priority: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.SET_PACKAGE_PRIORITY, packageId, priority),
skipItems: (itemIds: string[]): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.SKIP_ITEMS, itemIds), skipItems: (itemIds: string[]): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.SKIP_ITEMS, itemIds),
resetItems: (itemIds: string[]): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.RESET_ITEMS, itemIds),
startItems: (itemIds: string[]): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.START_ITEMS, itemIds),
onStateUpdate: (callback: (snapshot: UiSnapshot) => void): (() => void) => { onStateUpdate: (callback: (snapshot: UiSnapshot) => void): (() => void) => {
const listener = (_event: unknown, snapshot: UiSnapshot): void => callback(snapshot); const listener = (_event: unknown, snapshot: UiSnapshot): void => callback(snapshot);
ipcRenderer.on(IPC_CHANNELS.STATE_UPDATE, listener); ipcRenderer.on(IPC_CHANNELS.STATE_UPDATE, listener);

View File

@ -2833,10 +2833,13 @@ export function App(): ReactElement {
const hasItems = [...selectedIds].some((id) => snapshot.session.items[id]); const hasItems = [...selectedIds].some((id) => snapshot.session.items[id]);
return ( return (
<div ref={ctxMenuRef} className="ctx-menu" style={{ left: contextMenu.x, top: contextMenu.y }} onClick={(e) => e.stopPropagation()}> <div ref={ctxMenuRef} className="ctx-menu" style={{ left: contextMenu.x, top: contextMenu.y }} onClick={(e) => e.stopPropagation()}>
{(!contextMenu.itemId || multi) && hasPackages && ( {(hasPackages || hasItems) && (
<button className="ctx-menu-item" onClick={() => { <button className="ctx-menu-item" onClick={() => {
const pkgIds = [...selectedIds].filter((id) => snapshot.session.packages[id]); const pkgIds = [...selectedIds].filter((id) => snapshot.session.packages[id]);
void window.rd.startPackages(pkgIds); setContextMenu(null); const itemIds = [...selectedIds].filter((id) => snapshot.session.items[id]);
if (pkgIds.length > 0) void window.rd.startPackages(pkgIds);
if (itemIds.length > 0) void window.rd.startItems(itemIds);
setContextMenu(null);
}}>Ausgewählte Downloads starten{multi ? ` (${selectedIds.size})` : ""}</button> }}>Ausgewählte Downloads starten{multi ? ` (${selectedIds.size})` : ""}</button>
)} )}
<button className="ctx-menu-item" onClick={() => { void window.rd.start(); setContextMenu(null); }}>Alle Downloads starten</button> <button className="ctx-menu-item" onClick={() => { void window.rd.start(); setContextMenu(null); }}>Alle Downloads starten</button>

View File

@ -41,5 +41,7 @@ export const IPC_CHANNELS = {
CLEAR_HISTORY: "history:clear", CLEAR_HISTORY: "history:clear",
REMOVE_HISTORY_ENTRY: "history:remove-entry", REMOVE_HISTORY_ENTRY: "history:remove-entry",
SET_PACKAGE_PRIORITY: "queue:set-package-priority", 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; } as const;

View File

@ -34,7 +34,7 @@ export interface ElectronApi {
reorderPackages: (packageIds: string[]) => Promise<void>; reorderPackages: (packageIds: string[]) => Promise<void>;
removeItem: (itemId: string) => Promise<void>; removeItem: (itemId: string) => Promise<void>;
togglePackage: (packageId: string) => Promise<void>; togglePackage: (packageId: string) => Promise<void>;
exportQueue: () => Promise<string>; exportQueue: () => Promise<{ saved: boolean }>;
importQueue: (json: string) => Promise<{ addedPackages: number; addedLinks: number }>; importQueue: (json: string) => Promise<{ addedPackages: number; addedLinks: number }>;
toggleClipboard: () => Promise<boolean>; toggleClipboard: () => Promise<boolean>;
pickFolder: () => Promise<string | null>; pickFolder: () => Promise<string | null>;
@ -54,6 +54,8 @@ export interface ElectronApi {
removeHistoryEntry: (entryId: string) => Promise<void>; removeHistoryEntry: (entryId: string) => Promise<void>;
setPackagePriority: (packageId: string, priority: PackagePriority) => Promise<void>; setPackagePriority: (packageId: string, priority: PackagePriority) => Promise<void>;
skipItems: (itemIds: string[]) => Promise<void>; skipItems: (itemIds: string[]) => Promise<void>;
resetItems: (itemIds: string[]) => Promise<void>;
startItems: (itemIds: string[]) => Promise<void>;
onStateUpdate: (callback: (snapshot: UiSnapshot) => void) => () => void; onStateUpdate: (callback: (snapshot: UiSnapshot) => void) => () => void;
onClipboardDetected: (callback: (links: string[]) => void) => () => void; onClipboardDetected: (callback: (links: string[]) => void) => () => void;
onUpdateInstallProgress: (callback: (progress: UpdateInstallProgress) => void) => () => void; onUpdateInstallProgress: (callback: (progress: UpdateInstallProgress) => void) => () => void;