Release v1.6.3

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-03-04 05:54:41 +01:00
parent 1d4a13466f
commit 693f7b482a
4 changed files with 31 additions and 23 deletions

View File

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

@ -286,6 +286,9 @@ export class AppController {
this.settings = restoredSettings; this.settings = restoredSettings;
saveSettings(this.storagePaths, this.settings); saveSettings(this.storagePaths, this.settings);
this.manager.setSettings(this.settings); this.manager.setSettings(this.settings);
// Stop the manager BEFORE saving the restored session to prevent
// the auto-save timer from overwriting it with the old in-memory session.
this.manager.stop();
const restoredSession = parsed.session as ReturnType<typeof loadSession>; const restoredSession = parsed.session as ReturnType<typeof loadSession>;
saveSession(this.storagePaths, restoredSession); saveSession(this.storagePaths, restoredSession);
return { restored: true, message: "Backup wiederhergestellt. Bitte App neustarten." }; return { restored: true, message: "Backup wiederhergestellt. Bitte App neustarten." };

View File

@ -2576,8 +2576,6 @@ export class DownloadManager extends EventEmitter {
const p = this.session.packages[order[i]]; const p = this.session.packages[order[i]];
if (p && p.priority === "high") { if (p && p.priority === "high") {
insertAt = i + 1; insertAt = i + 1;
} else {
break;
} }
} }
order.splice(insertAt, 0, packageId); order.splice(insertAt, 0, packageId);
@ -2890,6 +2888,7 @@ export class DownloadManager extends EventEmitter {
// duration, average speed, and ETA are calculated relative to the current run, not cumulative. // duration, average speed, and ETA are calculated relative to the current run, not cumulative.
this.session.runStartedAt = nowMs(); this.session.runStartedAt = nowMs();
this.session.totalDownloadedBytes = 0; this.session.totalDownloadedBytes = 0;
this.sessionDownloadedBytes = 0;
this.session.summaryText = ""; this.session.summaryText = "";
this.session.reconnectUntil = 0; this.session.reconnectUntil = 0;
this.session.reconnectReason = ""; this.session.reconnectReason = "";
@ -3110,8 +3109,8 @@ export class DownloadManager extends EventEmitter {
} }
// Clear stale transient status texts from previous session // Clear stale transient status texts from previous session
if (item.status === "queued") { if (item.status === "queued") {
const fs = (item.fullStatus || "").trim(); const statusText = (item.fullStatus || "").trim();
if (fs !== "Wartet" && fs !== "Paket gestoppt" && fs !== "Online") { if (statusText !== "Wartet" && statusText !== "Paket gestoppt" && statusText !== "Online") {
item.fullStatus = "Wartet"; item.fullStatus = "Wartet";
} }
} }
@ -3120,8 +3119,8 @@ export class DownloadManager extends EventEmitter {
item.onlineStatus = undefined; item.onlineStatus = undefined;
} }
if (item.status === "completed") { if (item.status === "completed") {
const fs = (item.fullStatus || "").trim(); const statusText = (item.fullStatus || "").trim();
if (fs && !isExtractedLabel(fs) && !/^Fertig\b/i.test(fs)) { if (statusText && !isExtractedLabel(statusText) && !/^Fertig\b/i.test(statusText)) {
item.fullStatus = `Fertig (${humanSize(item.downloadedBytes)})`; item.fullStatus = `Fertig (${humanSize(item.downloadedBytes)})`;
} }
} }
@ -3165,7 +3164,7 @@ export class DownloadManager extends EventEmitter {
if (failed > 0) { if (failed > 0) {
pkg.status = "failed"; pkg.status = "failed";
} else if (cancelled > 0) { } else if (cancelled > 0) {
pkg.status = success > 0 ? "failed" : "cancelled"; pkg.status = success > 0 ? "completed" : "cancelled";
} else if (success > 0) { } else if (success > 0) {
pkg.status = "completed"; pkg.status = "completed";
} }
@ -4927,9 +4926,12 @@ export class DownloadManager extends EventEmitter {
if (writeMode === "w" && item.totalBytes && item.totalBytes > 0 && process.platform === "win32") { if (writeMode === "w" && item.totalBytes && item.totalBytes > 0 && process.platform === "win32") {
try { try {
const fd = await fs.promises.open(effectiveTargetPath, "w"); const fd = await fs.promises.open(effectiveTargetPath, "w");
await fd.truncate(item.totalBytes); try {
await fd.close(); await fd.truncate(item.totalBytes);
preAllocated = true; preAllocated = true;
} finally {
await fd.close();
}
} catch { /* best-effort */ } } catch { /* best-effort */ }
} }
@ -5281,6 +5283,11 @@ export class DownloadManager extends EventEmitter {
if (!stream.destroyed) { if (!stream.destroyed) {
stream.destroy(); stream.destroy();
} }
// If the body read succeeded but the final flush or stream close failed,
// propagate the error so the download is retried instead of marked complete.
if (bodyError) {
throw bodyError;
}
} }
// Detect tiny error-response files (e.g. hoster returning "Forbidden" with HTTP 200). // Detect tiny error-response files (e.g. hoster returning "Forbidden" with HTTP 200).
@ -6330,7 +6337,7 @@ export class DownloadManager extends EventEmitter {
} else if (failed > 0) { } else if (failed > 0) {
pkg.status = "failed"; pkg.status = "failed";
} else if (cancelled > 0) { } else if (cancelled > 0) {
pkg.status = success > 0 ? "failed" : "cancelled"; pkg.status = success > 0 ? "completed" : "cancelled";
} else { } else {
pkg.status = "completed"; pkg.status = "completed";
} }
@ -6504,21 +6511,19 @@ export class DownloadManager extends EventEmitter {
if (event) { if (event) {
bandwidthSamples.push({ bandwidthSamples.push({
timestamp: event.at, timestamp: event.at,
speedBps: event.bytes * 3 speedBps: Math.floor(event.bytes * (1000 / 120))
}); });
} }
} }
const paused = this.session.running && this.session.paused; const paused = this.session.running && this.session.paused;
const currentSpeedBps = paused ? 0 : this.speedBytesLastWindow / 3; const currentSpeedBps = paused ? 0 : this.speedBytesLastWindow / SPEED_WINDOW_SECONDS;
let totalBytes = 0;
let maxSpeed = 0; let maxSpeed = 0;
for (let i = this.speedEventsHead; i < this.speedEvents.length; i += 1) { for (let i = this.speedEventsHead; i < this.speedEvents.length; i += 1) {
const event = this.speedEvents[i]; const event = this.speedEvents[i];
if (event) { if (event) {
totalBytes += event.bytes; const speed = Math.floor(event.bytes * (1000 / 120));
const speed = event.bytes * 3;
if (speed > maxSpeed) { if (speed > maxSpeed) {
maxSpeed = speed; maxSpeed = speed;
} }

View File

@ -233,9 +233,9 @@ const BandwidthChart = memo(function BandwidthChart({ items, running, paused, sp
ctx.textAlign = "center"; ctx.textAlign = "center";
ctx.textBaseline = "top"; ctx.textBaseline = "top";
ctx.fillText("0s", padding.left, height - padding.bottom + 8); ctx.fillText("60s", padding.left, height - padding.bottom + 8);
ctx.fillText("30s", padding.left + chartWidth / 2, height - padding.bottom + 8); ctx.fillText("30s", padding.left + chartWidth / 2, height - padding.bottom + 8);
ctx.fillText("60s", width - padding.right, height - padding.bottom + 8); ctx.fillText("0s", width - padding.right, height - padding.bottom + 8);
if (history.length < 2) { if (history.length < 2) {
ctx.fillStyle = textColor; ctx.fillStyle = textColor;
@ -2320,9 +2320,9 @@ export function App(): ReactElement {
</span> </span>
{selectedHistoryIds.size > 0 && ( {selectedHistoryIds.size > 0 && (
<button className="btn btn-danger" onClick={() => { <button className="btn btn-danger" onClick={() => {
const ids = [...selectedHistoryIds]; const idSet = new Set(selectedHistoryIds);
void Promise.all(ids.map(id => window.rd.removeHistoryEntry(id))).then(() => { void Promise.all([...idSet].map(id => window.rd.removeHistoryEntry(id))).then(() => {
setHistoryEntries((prev) => prev.filter((e) => !selectedHistoryIds.has(e.id))); setHistoryEntries((prev) => prev.filter((e) => !idSet.has(e.id)));
setSelectedHistoryIds(new Set()); setSelectedHistoryIds(new Set());
}); });
}}>Ausgewählte entfernen ({selectedHistoryIds.size})</button> }}>Ausgewählte entfernen ({selectedHistoryIds.size})</button>
@ -3091,7 +3091,7 @@ const PackageCard = memo(function PackageCard({ pkg, items, packageSpeed, isFirs
const failed = items.filter((item) => item.status === "failed").length; const failed = items.filter((item) => item.status === "failed").length;
const cancelled = items.filter((item) => item.status === "cancelled").length; const cancelled = items.filter((item) => item.status === "cancelled").length;
const extracted = items.filter((item) => item.fullStatus?.startsWith("Entpackt")).length; const extracted = items.filter((item) => item.fullStatus?.startsWith("Entpackt")).length;
const extracting = items.some((item) => item.fullStatus?.startsWith("Entpack")); const extracting = items.some((item) => item.fullStatus?.startsWith("Entpacken"));
const total = Math.max(1, items.length); const total = Math.max(1, items.length);
// Use 50/50 split when extraction is active OR package is in extracting state // Use 50/50 split when extraction is active OR package is in extracting state
// (prevents bar jumping from 100% to 50% when extraction starts) // (prevents bar jumping from 100% to 50% when extraction starts)