Release v1.6.19

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-03-04 17:45:30 +01:00
parent a263e3eb2c
commit b8bbc9c32f
5 changed files with 60 additions and 15 deletions

View File

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

@ -1437,6 +1437,21 @@ export class DownloadManager extends EventEmitter {
this.persistSoon(); this.persistSoon();
this.emitState(true); 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 }; return { skipped: true, overwritten: false };
} }
@ -2472,8 +2487,8 @@ export class DownloadManager extends EventEmitter {
const active = this.activeTasks.get(itemId); const active = this.activeTasks.get(itemId);
if (active) { if (active) {
active.abortReason = "cancel"; active.abortReason = "reset";
active.abortController.abort("cancel"); active.abortController.abort("reset");
} }
// Delete partial download file // Delete partial download file
@ -2540,8 +2555,8 @@ export class DownloadManager extends EventEmitter {
const active = this.activeTasks.get(itemId); const active = this.activeTasks.get(itemId);
if (active) { if (active) {
active.abortReason = "cancel"; active.abortReason = "reset";
active.abortController.abort("cancel"); active.abortController.abort("reset");
} }
const targetPath = String(item.targetPath || "").trim(); const targetPath = String(item.targetPath || "").trim();
@ -2552,7 +2567,6 @@ export class DownloadManager extends EventEmitter {
this.dropItemContribution(itemId); this.dropItemContribution(itemId);
this.runOutcomes.delete(itemId); this.runOutcomes.delete(itemId);
this.runItemIds.delete(itemId);
this.retryAfterByItem.delete(itemId); this.retryAfterByItem.delete(itemId);
this.retryStateByItem.delete(itemId); this.retryStateByItem.delete(itemId);
@ -2570,6 +2584,13 @@ export class DownloadManager extends EventEmitter {
item.fullStatus = "Wartet"; item.fullStatus = "Wartet";
item.onlineStatus = undefined; item.onlineStatus = undefined;
item.updatedAt = nowMs(); 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) // 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), genericErrorRetries: Number(active.genericErrorRetries || 0),
unrestrictRetries: Number(active.unrestrictRetries || 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") { } else if (reason === "package_toggle") {
item.status = "queued"; item.status = "queued";
item.speedBps = 0; item.speedBps = 0;

View File

@ -130,7 +130,7 @@ function createTray(): void {
const contextMenu = Menu.buildFromTemplate([ const contextMenu = Menu.buildFromTemplate([
{ label: "Anzeigen", click: () => { mainWindow?.show(); mainWindow?.focus(); } }, { label: "Anzeigen", click: () => { mainWindow?.show(); mainWindow?.focus(); } },
{ type: "separator" }, { 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(); } }, { label: "Stop", click: () => { controller.stop(); } },
{ type: "separator" }, { type: "separator" },
{ label: "Beenden", click: () => { app.quit(); } } { label: "Beenden", click: () => { app.quit(); } }
@ -453,6 +453,11 @@ function registerIpcHandlers(): void {
return { restored: false, message: "Abgebrochen" }; return { restored: false, message: "Abgebrochen" };
} }
const filePath = result.filePaths[0]; 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"); const json = await fs.promises.readFile(filePath, "utf8");
return controller.importBackup(json); return controller.importBackup(json);
}); });

View File

@ -449,8 +449,13 @@ export function saveSettings(paths: StoragePaths, settings: AppSettings): void {
const persisted = sanitizeCredentialPersistence(normalizeSettings(settings)); const persisted = sanitizeCredentialPersistence(normalizeSettings(settings));
const payload = JSON.stringify(persisted, null, 2); const payload = JSON.stringify(persisted, null, 2);
const tempPath = `${paths.configFile}.tmp`; const tempPath = `${paths.configFile}.tmp`;
fs.writeFileSync(tempPath, payload, "utf8"); try {
syncRenameWithExdevFallback(tempPath, paths.configFile); fs.writeFileSync(tempPath, payload, "utf8");
syncRenameWithExdevFallback(tempPath, paths.configFile);
} catch (error) {
try { fs.rmSync(tempPath, { force: true }); } catch { /* ignore */ }
throw error;
}
} }
let asyncSettingsSaveRunning = false; let asyncSettingsSaveRunning = false;
@ -553,8 +558,13 @@ export function saveSession(paths: StoragePaths, session: SessionState): void {
} }
const payload = JSON.stringify({ ...session, updatedAt: Date.now() }); const payload = JSON.stringify({ ...session, updatedAt: Date.now() });
const tempPath = sessionTempPath(paths.sessionFile, "sync"); const tempPath = sessionTempPath(paths.sessionFile, "sync");
fs.writeFileSync(tempPath, payload, "utf8"); try {
syncRenameWithExdevFallback(tempPath, paths.sessionFile); fs.writeFileSync(tempPath, payload, "utf8");
syncRenameWithExdevFallback(tempPath, paths.sessionFile);
} catch (error) {
try { fs.rmSync(tempPath, { force: true }); } catch { /* ignore */ }
throw error;
}
} }
let asyncSaveRunning = false; let asyncSaveRunning = false;
@ -652,8 +662,13 @@ export function saveHistory(paths: StoragePaths, entries: HistoryEntry[]): void
const trimmed = entries.slice(0, MAX_HISTORY_ENTRIES); const trimmed = entries.slice(0, MAX_HISTORY_ENTRIES);
const payload = JSON.stringify(trimmed, null, 2); const payload = JSON.stringify(trimmed, null, 2);
const tempPath = `${paths.historyFile}.tmp`; const tempPath = `${paths.historyFile}.tmp`;
fs.writeFileSync(tempPath, payload, "utf8"); try {
syncRenameWithExdevFallback(tempPath, paths.historyFile); 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[] { export function addHistoryEntry(paths: StoragePaths, entry: HistoryEntry): HistoryEntry[] {

View File

@ -2726,9 +2726,9 @@ export function App(): ReactElement {
const speedInput = scheduleSpeedInputs[scheduleKey] ?? formatMbpsInputFromKbps(s.speedLimitKbps); const speedInput = scheduleSpeedInputs[scheduleKey] ?? formatMbpsInputFromKbps(s.speedLimitKbps);
return ( return (
<div key={scheduleKey} className="schedule-row"> <div key={scheduleKey} className="schedule-row">
<input type="number" min={0} max={23} value={s.startHour} onChange={(e) => updateSchedule(i, "startHour", Number(e.target.value))} title="Von (Stunde)" /> <input type="number" min={0} max={23} value={s.startHour} onChange={(e) => { const v = Number(e.target.value); if (!Number.isNaN(v)) updateSchedule(i, "startHour", Math.max(0, Math.min(23, v))); }} title="Von (Stunde)" />
<span>-</span> <span>-</span>
<input type="number" min={0} max={23} value={s.endHour} onChange={(e) => updateSchedule(i, "endHour", Number(e.target.value))} title="Bis (Stunde)" /> <input type="number" min={0} max={23} value={s.endHour} onChange={(e) => { const v = Number(e.target.value); if (!Number.isNaN(v)) updateSchedule(i, "endHour", Math.max(0, Math.min(23, v))); }} title="Bis (Stunde)" />
<span>Uhr</span> <span>Uhr</span>
<input type="number" min={0} step={0.1} value={speedInput} onChange={(event) => { 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)" /> <input type="number" min={0} step={0.1} value={speedInput} onChange={(event) => { 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)" />
<span>MB/s</span> <span>MB/s</span>
@ -2825,6 +2825,7 @@ export function App(): ReactElement {
<button className="btn danger" onClick={() => { <button className="btn danger" onClick={() => {
if (deleteConfirm.dontAsk) { if (deleteConfirm.dontAsk) {
setBool("confirmDeleteSelection", false); setBool("confirmDeleteSelection", false);
void window.rd.updateSettings({ confirmDeleteSelection: false }).catch(() => {});
} }
executeDeleteSelection(deleteConfirm.ids); executeDeleteSelection(deleteConfirm.ids);
setDeleteConfirm(null); setDeleteConfirm(null);