Release v1.5.89
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
a18ab484cc
commit
92101e249a
@ -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",
|
||||||
|
|||||||
@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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");
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user