Add "Jetzt entpacken" context menu, fix start freeze on large queues
Some checks are pending
Build and Release / build (push) Waiting to run

- New "Jetzt entpacken" right-click option: triggers extraction for
  completed packages regardless of paused/stopped state
- Fix 5-10s freeze when pressing Start after Pause: recoverRetryableItems
  was calling fs.stat on every item (474+); now only checks failed/completed
- Full IPC pipeline: extractNow in manager, controller, preload, renderer

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-03-02 20:40:49 +01:00
parent e013c63c59
commit 0b73ea1386
8 changed files with 48 additions and 7 deletions

View File

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

@ -212,6 +212,10 @@ export class AppController {
this.manager.retryExtraction(packageId); this.manager.retryExtraction(packageId);
} }
public extractNow(packageId: string): void {
this.manager.extractNow(packageId);
}
public cancelPackage(packageId: string): void { public cancelPackage(packageId: string): void {
this.manager.cancelPackage(packageId); this.manager.cancelPackage(packageId);
} }

View File

@ -2976,6 +2976,25 @@ export class DownloadManager extends EventEmitter {
void this.runPackagePostProcessing(packageId).catch((err) => logger.warn(`runPackagePostProcessing Fehler (retryExtraction): ${compactErrorText(err)}`)); void this.runPackagePostProcessing(packageId).catch((err) => logger.warn(`runPackagePostProcessing Fehler (retryExtraction): ${compactErrorText(err)}`));
} }
public extractNow(packageId: string): void {
const pkg = this.session.packages[packageId];
if (!pkg || pkg.cancelled) return;
if (this.packagePostProcessTasks.has(packageId)) return;
const items = pkg.itemIds.map((id) => this.session.items[id]).filter(Boolean) as DownloadItem[];
const completedItems = items.filter((item) => item.status === "completed");
if (completedItems.length === 0) return;
pkg.status = "queued";
pkg.updatedAt = nowMs();
for (const item of completedItems) {
item.fullStatus = "Entpacken - Ausstehend";
item.updatedAt = nowMs();
}
logger.info(`Jetzt entpacken: pkg=${pkg.name}, completed=${completedItems.length}`);
this.persistSoon();
this.emitState(true);
void this.runPackagePostProcessing(packageId).catch((err) => logger.warn(`runPackagePostProcessing Fehler (extractNow): ${compactErrorText(err)}`));
}
private removePackageFromSession(packageId: string, itemIds: string[]): void { private removePackageFromSession(packageId: string, itemIds: string[]): void {
const postProcessController = this.packagePostProcessAbortControllers.get(packageId); const postProcessController = this.packagePostProcessAbortControllers.get(packageId);
if (postProcessController && !postProcessController.signal.aborted) { if (postProcessController && !postProcessController.signal.aborted) {
@ -4422,11 +4441,16 @@ export class DownloadManager extends EventEmitter {
for (const itemId of pkg.itemIds) { for (const itemId of pkg.itemIds) {
const item = this.session.items[itemId]; const item = this.session.items[itemId];
if (!item || item.status === "cancelled" || this.activeTasks.has(itemId)) { if (!item || this.activeTasks.has(itemId)) {
continue;
}
// Only check failed or completed items — skip queued/cancelled to avoid
// expensive fs.stat calls on hundreds of items (caused 5-10s freeze on start).
if (item.status !== "failed" && item.status !== "completed") {
continue; continue;
} }
const is416Failure = this.isHttp416Failure(item); const is416Failure = item.status === "failed" && this.isHttp416Failure(item);
const hasZeroByteArchive = await this.hasZeroByteArchiveArtifact(item); const hasZeroByteArchive = await this.hasZeroByteArchiveArtifact(item);
if (item.status === "failed") { if (item.status === "failed") {

View File

@ -318,6 +318,10 @@ function registerIpcHandlers(): void {
validateString(packageId, "packageId"); validateString(packageId, "packageId");
return controller.retryExtraction(packageId); return controller.retryExtraction(packageId);
}); });
ipcMain.handle(IPC_CHANNELS.EXTRACT_NOW, (_event: IpcMainInvokeEvent, packageId: string) => {
validateString(packageId, "packageId");
return controller.extractNow(packageId);
});
ipcMain.handle(IPC_CHANNELS.EXPORT_QUEUE, () => controller.exportQueue()); ipcMain.handle(IPC_CHANNELS.EXPORT_QUEUE, () => controller.exportQueue());
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");

View File

@ -48,6 +48,7 @@ const api: ElectronApi = {
importBackup: (): Promise<{ restored: boolean; message: string }> => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_BACKUP), importBackup: (): Promise<{ restored: boolean; message: string }> => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_BACKUP),
openLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_LOG), openLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_LOG),
retryExtraction: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.RETRY_EXTRACTION, packageId), retryExtraction: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.RETRY_EXTRACTION, packageId),
extractNow: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.EXTRACT_NOW, packageId),
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

@ -2569,9 +2569,15 @@ export function App(): ReactElement {
const pkg = snapshot.session.packages[contextMenu.packageId]; const pkg = snapshot.session.packages[contextMenu.packageId];
const items = pkg?.itemIds.map((id) => snapshot.session.items[id]).filter(Boolean) || []; const items = pkg?.itemIds.map((id) => snapshot.session.items[id]).filter(Boolean) || [];
const hasExtractError = items.some((item) => item && /^Entpack-Fehler/i.test(item.fullStatus)); const hasExtractError = items.some((item) => item && /^Entpack-Fehler/i.test(item.fullStatus));
return hasExtractError ? ( const allCompleted = items.length > 0 && items.every((item) => item && item.status === "completed");
<button className="ctx-menu-item" onClick={() => { void window.rd.retryExtraction(contextMenu.packageId); setContextMenu(null); }}>Extraktion wiederholen</button> return (<>
) : null; {allCompleted && (
<button className="ctx-menu-item" onClick={() => { void window.rd.extractNow(contextMenu.packageId); setContextMenu(null); }}>Jetzt entpacken</button>
)}
{hasExtractError && (
<button className="ctx-menu-item" onClick={() => { void window.rd.retryExtraction(contextMenu.packageId); setContextMenu(null); }}>Extraktion wiederholen</button>
)}
</>);
})()} })()}
{hasPackages && ( {hasPackages && (
<button className="ctx-menu-item ctx-danger" onClick={() => { <button className="ctx-menu-item ctx-danger" onClick={() => {

View File

@ -32,5 +32,6 @@ export const IPC_CHANNELS = {
EXPORT_BACKUP: "app:export-backup", EXPORT_BACKUP: "app:export-backup",
IMPORT_BACKUP: "app:import-backup", IMPORT_BACKUP: "app:import-backup",
OPEN_LOG: "app:open-log", OPEN_LOG: "app:open-log",
RETRY_EXTRACTION: "queue:retry-extraction" RETRY_EXTRACTION: "queue:retry-extraction",
EXTRACT_NOW: "queue:extract-now"
} as const; } as const;

View File

@ -43,6 +43,7 @@ export interface ElectronApi {
importBackup: () => Promise<{ restored: boolean; message: string }>; importBackup: () => Promise<{ restored: boolean; message: string }>;
openLog: () => Promise<void>; openLog: () => Promise<void>;
retryExtraction: (packageId: string) => Promise<void>; retryExtraction: (packageId: string) => Promise<void>;
extractNow: (packageId: 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;