Release v1.6.29

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-03-04 21:11:32 +01:00
parent 20a0a59670
commit 1d0ee31001
5 changed files with 59 additions and 8 deletions

View File

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

@ -314,6 +314,9 @@ export class AppController {
normalizeLoadedSession(parsed.session) normalizeLoadedSession(parsed.session)
); );
saveSession(this.storagePaths, restoredSession); saveSession(this.storagePaths, restoredSession);
// Prevent prepareForShutdown from overwriting the restored session file
// with the old in-memory session when the app quits after backup restore.
this.manager.skipShutdownPersist = true;
return { restored: true, message: "Backup wiederhergestellt. Bitte App neustarten." }; return { restored: true, message: "Backup wiederhergestellt. Bitte App neustarten." };
} }

View File

@ -801,6 +801,8 @@ export class DownloadManager extends EventEmitter {
private storagePaths: StoragePaths; private storagePaths: StoragePaths;
public skipShutdownPersist = false;
private debridService: DebridService; private debridService: DebridService;
private invalidateMegaSessionFn?: () => void; private invalidateMegaSessionFn?: () => void;
@ -2390,7 +2392,8 @@ export class DownloadManager extends EventEmitter {
const baseName = sanitizeFilename(parsed.name || "video"); const baseName = sanitizeFilename(parsed.name || "video");
let index = 1; let index = 1;
while (true) { const MAX_ATTEMPTS = 10000;
while (index <= MAX_ATTEMPTS) {
const candidateName = index <= 1 const candidateName = index <= 1
? `${baseName}${extension}` ? `${baseName}${extension}`
: `${baseName} (${index})${extension}`; : `${baseName} (${index})${extension}`;
@ -2406,6 +2409,11 @@ export class DownloadManager extends EventEmitter {
} }
index += 1; index += 1;
} }
// Fallback: use timestamp-based name to guarantee termination
const fallbackName = `${baseName} (${Date.now()})${extension}`;
const fallbackPath = path.join(targetDir, fallbackName);
reserved.add(pathKey(fallbackPath));
return fallbackPath;
} }
private async collectMkvFilesToLibrary(packageId: string, pkg: PackageEntry): Promise<void> { private async collectMkvFilesToLibrary(packageId: string, pkg: PackageEntry): Promise<void> {
@ -2821,6 +2829,9 @@ export class DownloadManager extends EventEmitter {
this.runCompletedPackages.clear(); this.runCompletedPackages.clear();
this.retryAfterByItem.clear(); this.retryAfterByItem.clear();
this.retryStateByItem.clear(); this.retryStateByItem.clear();
this.itemContributedBytes.clear();
this.reservedTargetPaths.clear();
this.claimedTargetPathByItem.clear();
this.session.running = true; this.session.running = true;
this.session.paused = false; this.session.paused = false;
this.session.runStartedAt = nowMs(); this.session.runStartedAt = nowMs();
@ -3047,6 +3058,9 @@ export class DownloadManager extends EventEmitter {
this.runCompletedPackages.clear(); this.runCompletedPackages.clear();
this.retryAfterByItem.clear(); this.retryAfterByItem.clear();
this.retryStateByItem.clear(); this.retryStateByItem.clear();
this.itemContributedBytes.clear();
this.reservedTargetPaths.clear();
this.claimedTargetPathByItem.clear();
this.session.running = true; this.session.running = true;
this.session.paused = false; this.session.paused = false;
@ -3195,8 +3209,11 @@ export class DownloadManager extends EventEmitter {
this.nonResumableActive = 0; this.nonResumableActive = 0;
this.session.summaryText = ""; this.session.summaryText = "";
// Persist synchronously on shutdown to guarantee data is written before process exits // Persist synchronously on shutdown to guarantee data is written before process exits
saveSession(this.storagePaths, this.session); // Skip if a backup was just imported — the restored session on disk must not be overwritten
saveSettings(this.storagePaths, this.settings); if (!this.skipShutdownPersist) {
saveSession(this.storagePaths, this.session);
saveSettings(this.storagePaths, this.settings);
}
this.emitState(true); this.emitState(true);
logger.info(`Shutdown-Vorbereitung beendet: requeued=${requeuedItems}`); logger.info(`Shutdown-Vorbereitung beendet: requeued=${requeuedItems}`);
} }
@ -6056,8 +6073,16 @@ export class DownloadManager extends EventEmitter {
} }
if (item.status === "completed" && item.targetPath) { if (item.status === "completed" && item.targetPath) {
completedPaths.add(pathKey(item.targetPath)); completedPaths.add(pathKey(item.targetPath));
} else if (item.targetPath) { } else {
pendingPaths.add(pathKey(item.targetPath)); if (item.targetPath) {
pendingPaths.add(pathKey(item.targetPath));
}
// Items that haven't started yet have no targetPath but may have a fileName.
// Include their projected path so the archive-readiness check doesn't
// prematurely trigger extraction while parts are still queued.
if (item.fileName && pkg.outputDir) {
pendingPaths.add(pathKey(path.join(pkg.outputDir, item.fileName)));
}
} }
} }
if (completedPaths.size === 0) { if (completedPaths.size === 0) {
@ -6408,13 +6433,21 @@ export class DownloadManager extends EventEmitter {
const errorText = String(error || ""); const errorText = String(error || "");
if (errorText.includes("aborted:extract")) { if (errorText.includes("aborted:extract")) {
logger.info(`Hybrid-Extract abgebrochen: pkg=${pkg.name}`); logger.info(`Hybrid-Extract abgebrochen: pkg=${pkg.name}`);
const abortAt = nowMs();
for (const entry of hybridItems) {
if (isExtractedLabel(entry.fullStatus || "")) continue;
if (/^Entpacken\b/i.test(entry.fullStatus || "") || /^Passwort\b/i.test(entry.fullStatus || "")) {
entry.fullStatus = "Entpacken abgebrochen (wird fortgesetzt)";
entry.updatedAt = abortAt;
}
}
return; return;
} }
logger.warn(`Hybrid-Extract Fehler: pkg=${pkg.name}, reason=${compactErrorText(error)}`); logger.warn(`Hybrid-Extract Fehler: pkg=${pkg.name}, reason=${compactErrorText(error)}`);
const errorAt = nowMs(); const errorAt = nowMs();
for (const entry of hybridItems) { for (const entry of hybridItems) {
if (isExtractedLabel(entry.fullStatus || "")) continue; if (isExtractedLabel(entry.fullStatus || "")) continue;
if (/^Entpacken\b/i.test(entry.fullStatus || "") || /^Passwort\b/i.test(entry.fullStatus || "") || entry.fullStatus === "Entpacken - Ausstehend" || entry.fullStatus === "Entpacken - Warten auf Parts") { if (/^Entpacken\b/i.test(entry.fullStatus || "") || /^Passwort\b/i.test(entry.fullStatus || "")) {
entry.fullStatus = `Entpacken - Error`; entry.fullStatus = `Entpacken - Error`;
entry.updatedAt = errorAt; entry.updatedAt = errorAt;
} }
@ -6892,6 +6925,7 @@ export class DownloadManager extends EventEmitter {
} }
private finishRun(): void { private finishRun(): void {
const runStartedAt = this.session.runStartedAt;
this.session.running = false; this.session.running = false;
this.session.paused = false; this.session.paused = false;
this.session.runStartedAt = 0; this.session.runStartedAt = 0;
@ -6901,7 +6935,7 @@ export class DownloadManager extends EventEmitter {
const failed = outcomes.filter((status) => status === "failed").length; const failed = outcomes.filter((status) => status === "failed").length;
const cancelled = outcomes.filter((status) => status === "cancelled").length; const cancelled = outcomes.filter((status) => status === "cancelled").length;
const extracted = this.runCompletedPackages.size; const extracted = this.runCompletedPackages.size;
const duration = this.session.runStartedAt > 0 ? Math.max(1, Math.floor((nowMs() - this.session.runStartedAt) / 1000)) : 1; const duration = runStartedAt > 0 ? Math.max(1, Math.floor((nowMs() - runStartedAt) / 1000)) : 1;
const avgSpeed = Math.floor(this.session.totalDownloadedBytes / duration); const avgSpeed = Math.floor(this.session.totalDownloadedBytes / duration);
this.summary = { this.summary = {
total, total,

View File

@ -1637,6 +1637,8 @@ export function collectArchiveCleanupTargets(sourceArchivePath: string, director
if (multipartRar) { if (multipartRar) {
const prefix = escapeRegex(multipartRar[1]); const prefix = escapeRegex(multipartRar[1]);
addMatching(new RegExp(`^${prefix}\\.part\\d+\\.rar$`, "i")); addMatching(new RegExp(`^${prefix}\\.part\\d+\\.rar$`, "i"));
// RAR5 recovery volumes: prefix.partN.rev AND legacy prefix.rev
addMatching(new RegExp(`^${prefix}\\.part\\d+\\.rev$`, "i"));
addMatching(new RegExp(`^${prefix}\\.rev$`, "i")); addMatching(new RegExp(`^${prefix}\\.rev$`, "i"));
return Array.from(targets); return Array.from(targets);
} }

View File

@ -424,6 +424,18 @@ export function normalizeLoadedSessionTransientFields(session: SessionState): Se
item.speedBps = 0; item.speedBps = 0;
} }
// Reset package-level active statuses to queued (mirrors item reset above)
const ACTIVE_PKG_STATUSES = new Set(["downloading", "validating", "extracting", "integrity_check", "paused", "reconnect_wait"]);
for (const pkg of Object.values(session.packages)) {
if (ACTIVE_PKG_STATUSES.has(pkg.status)) {
pkg.status = "queued";
}
}
// Clear stale session-level running/paused flags
session.running = false;
session.paused = false;
return session; return session;
} }