diff --git a/package.json b/package.json index 0ddadd5..6b9a3f5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.6.18", + "version": "1.6.19", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 601db78..3225d4f 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -1437,6 +1437,21 @@ export class DownloadManager extends EventEmitter { this.persistSoon(); this.emitState(true); + + // Fix async race: processItem catch with "package_toggle" overwrites fullStatus + // after we already set it to "Wartet". Re-fix on next microtask. + const pkgItemIds = [...pkg.itemIds]; + queueMicrotask(() => { + for (const iid of pkgItemIds) { + const it = this.session.items[iid]; + if (it && it.status === "queued" && it.fullStatus === "Paket gestoppt") { + it.fullStatus = "Wartet"; + it.updatedAt = nowMs(); + } + } + this.emitState(true); + }); + return { skipped: true, overwritten: false }; } @@ -2472,8 +2487,8 @@ export class DownloadManager extends EventEmitter { const active = this.activeTasks.get(itemId); if (active) { - active.abortReason = "cancel"; - active.abortController.abort("cancel"); + active.abortReason = "reset"; + active.abortController.abort("reset"); } // Delete partial download file @@ -2540,8 +2555,8 @@ export class DownloadManager extends EventEmitter { const active = this.activeTasks.get(itemId); if (active) { - active.abortReason = "cancel"; - active.abortController.abort("cancel"); + active.abortReason = "reset"; + active.abortController.abort("reset"); } const targetPath = String(item.targetPath || "").trim(); @@ -2552,7 +2567,6 @@ export class DownloadManager extends EventEmitter { this.dropItemContribution(itemId); this.runOutcomes.delete(itemId); - this.runItemIds.delete(itemId); this.retryAfterByItem.delete(itemId); this.retryStateByItem.delete(itemId); @@ -2570,6 +2584,13 @@ export class DownloadManager extends EventEmitter { item.fullStatus = "Wartet"; item.onlineStatus = undefined; item.updatedAt = nowMs(); + + // Re-add to runItemIds if session is running so outcome is tracked in summary + if (this.session.running) { + this.runItemIds.add(itemId); + } else { + this.runItemIds.delete(itemId); + } } // Reset parent package status if it was completed/failed (now has queued items again) @@ -4678,6 +4699,9 @@ export class DownloadManager extends EventEmitter { genericErrorRetries: Number(active.genericErrorRetries || 0), unrestrictRetries: Number(active.unrestrictRetries || 0) }); + } else if (reason === "reset") { + // Item was reset externally by resetItems/resetPackage — state already set, do nothing + this.retryStateByItem.delete(item.id); } else if (reason === "package_toggle") { item.status = "queued"; item.speedBps = 0; diff --git a/src/main/main.ts b/src/main/main.ts index 2049632..5b1267d 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -130,7 +130,7 @@ function createTray(): void { const contextMenu = Menu.buildFromTemplate([ { label: "Anzeigen", click: () => { mainWindow?.show(); mainWindow?.focus(); } }, { type: "separator" }, - { label: "Start", click: () => { controller.start(); } }, + { label: "Start", click: () => { void controller.start().catch((err) => logger.warn(`Tray Start Fehler: ${String(err)}`)); } }, { label: "Stop", click: () => { controller.stop(); } }, { type: "separator" }, { label: "Beenden", click: () => { app.quit(); } } @@ -453,6 +453,11 @@ function registerIpcHandlers(): void { return { restored: false, message: "Abgebrochen" }; } const filePath = result.filePaths[0]; + const stat = await fs.promises.stat(filePath); + const BACKUP_MAX_BYTES = 50 * 1024 * 1024; + if (stat.size > BACKUP_MAX_BYTES) { + return { restored: false, message: `Backup-Datei zu groß (max 50 MB, Datei hat ${(stat.size / 1024 / 1024).toFixed(1)} MB)` }; + } const json = await fs.promises.readFile(filePath, "utf8"); return controller.importBackup(json); }); diff --git a/src/main/storage.ts b/src/main/storage.ts index b261eff..f3ee70d 100644 --- a/src/main/storage.ts +++ b/src/main/storage.ts @@ -449,8 +449,13 @@ export function saveSettings(paths: StoragePaths, settings: AppSettings): void { const persisted = sanitizeCredentialPersistence(normalizeSettings(settings)); const payload = JSON.stringify(persisted, null, 2); const tempPath = `${paths.configFile}.tmp`; - fs.writeFileSync(tempPath, payload, "utf8"); - syncRenameWithExdevFallback(tempPath, paths.configFile); + try { + fs.writeFileSync(tempPath, payload, "utf8"); + syncRenameWithExdevFallback(tempPath, paths.configFile); + } catch (error) { + try { fs.rmSync(tempPath, { force: true }); } catch { /* ignore */ } + throw error; + } } let asyncSettingsSaveRunning = false; @@ -553,8 +558,13 @@ export function saveSession(paths: StoragePaths, session: SessionState): void { } const payload = JSON.stringify({ ...session, updatedAt: Date.now() }); const tempPath = sessionTempPath(paths.sessionFile, "sync"); - fs.writeFileSync(tempPath, payload, "utf8"); - syncRenameWithExdevFallback(tempPath, paths.sessionFile); + try { + fs.writeFileSync(tempPath, payload, "utf8"); + syncRenameWithExdevFallback(tempPath, paths.sessionFile); + } catch (error) { + try { fs.rmSync(tempPath, { force: true }); } catch { /* ignore */ } + throw error; + } } let asyncSaveRunning = false; @@ -652,8 +662,13 @@ export function saveHistory(paths: StoragePaths, entries: HistoryEntry[]): void const trimmed = entries.slice(0, MAX_HISTORY_ENTRIES); const payload = JSON.stringify(trimmed, null, 2); const tempPath = `${paths.historyFile}.tmp`; - fs.writeFileSync(tempPath, payload, "utf8"); - syncRenameWithExdevFallback(tempPath, paths.historyFile); + try { + fs.writeFileSync(tempPath, payload, "utf8"); + syncRenameWithExdevFallback(tempPath, paths.historyFile); + } catch (error) { + try { fs.rmSync(tempPath, { force: true }); } catch { /* ignore */ } + throw error; + } } export function addHistoryEntry(paths: StoragePaths, entry: HistoryEntry): HistoryEntry[] { diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 79f865e..124f331 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -2726,9 +2726,9 @@ export function App(): ReactElement { const speedInput = scheduleSpeedInputs[scheduleKey] ?? formatMbpsInputFromKbps(s.speedLimitKbps); return (
- updateSchedule(i, "startHour", Number(e.target.value))} title="Von (Stunde)" /> + { const v = Number(e.target.value); if (!Number.isNaN(v)) updateSchedule(i, "startHour", Math.max(0, Math.min(23, v))); }} title="Von (Stunde)" /> - - updateSchedule(i, "endHour", Number(e.target.value))} title="Bis (Stunde)" /> + { const v = Number(e.target.value); if (!Number.isNaN(v)) updateSchedule(i, "endHour", Math.max(0, Math.min(23, v))); }} title="Bis (Stunde)" /> Uhr { setScheduleSpeedInputs((prev) => ({ ...prev, [scheduleKey]: event.target.value })); }} onBlur={(event) => { const parsed = parseMbpsInput(event.target.value); if (parsed === null) { setScheduleSpeedInputs((prev) => ({ ...prev, [scheduleKey]: formatMbpsInputFromKbps(s.speedLimitKbps) })); return; } const nextKbps = Math.floor(parsed * 1024); setScheduleSpeedInputs((prev) => ({ ...prev, [scheduleKey]: formatMbpsInputFromKbps(nextKbps) })); updateSchedule(i, "speedLimitKbps", nextKbps); }} title="MB/s (0=unbegrenzt)" /> MB/s @@ -2825,6 +2825,7 @@ export function App(): ReactElement {