Release v1.6.16

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-03-04 16:50:59 +01:00
parent 56c0b633c8
commit b02aef2af9
5 changed files with 44 additions and 19 deletions

View File

@ -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",

View File

@ -81,8 +81,15 @@ export class AppController {
void this.manager.getStartConflicts().then((conflicts) => {
const hasConflicts = conflicts.length > 0;
if (this.hasAnyProviderToken(this.settings) && !hasConflicts) {
// 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<typeof loadSession>;
saveSession(this.storagePaths, restoredSession);
return { restored: true, message: "Backup wiederhergestellt. Bitte App neustarten." };

View File

@ -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,8 +3558,12 @@ export class DownloadManager extends EventEmitter {
await new Promise<void>((resolve) => {
this.packagePostProcessWaiters.push({ packageId, resolve });
});
// 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 {
// Guard: stop() resets active to 0, but old tasks (aborted waiters) still
@ -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();
// 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();

View File

@ -50,6 +50,7 @@ process.on("unhandledRejection", (reason) => {
let mainWindow: BrowserWindow | null = null;
let tray: Tray | null = null;
let clipboardTimer: ReturnType<typeof setInterval> | null = null;
let updateQuitTimer: ReturnType<typeof setTimeout> | 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 {

View File

@ -2935,7 +2935,7 @@ export function App(): ReactElement {
{(hasPackages || hasStartableItems) && (
<button className="ctx-menu-item" onClick={() => {
const pkgIds = [...selectedIds].filter((id) => snapshot.session.packages[id]);
const itemIds = [...selectedIds].filter((id) => snapshot.session.items[id]);
const itemIds = [...selectedIds].filter((id) => { const it = snapshot.session.items[id]; return it && startableStatuses.has(it.status); });
if (pkgIds.length > 0) void window.rd.startPackages(pkgIds);
if (itemIds.length > 0) void window.rd.startItems(itemIds);
setContextMenu(null);
@ -3071,9 +3071,9 @@ export function App(): ReactElement {
const contextEntry = historyEntries.find(e => e.id === historyCtxMenu.entryId);
const hasUrls = (contextEntry?.urls?.length ?? 0) > 0;
const removeSelected = (): void => {
const ids = [...selectedHistoryIds];
void Promise.all(ids.map(id => window.rd.removeHistoryEntry(id))).then(() => {
setHistoryEntries((prev) => prev.filter((e) => !selectedHistoryIds.has(e.id)));
const idSet = new Set(selectedHistoryIds);
void Promise.all([...idSet].map(id => window.rd.removeHistoryEntry(id))).then(() => {
setHistoryEntries((prev) => prev.filter((e) => !idSet.has(e.id)));
setSelectedHistoryIds(new Set());
}).catch(() => {
void window.rd.getHistory().then((entries) => { setHistoryEntries(entries); setSelectedHistoryIds(new Set()); }).catch(() => {});