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",
"version": "1.6.28",
"version": "1.6.29",
"description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)",
"main": "build/main/main/main.js",
"author": "Sucukdeluxe",

View File

@ -314,6 +314,9 @@ export class AppController {
normalizeLoadedSession(parsed.session)
);
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." };
}

View File

@ -801,6 +801,8 @@ export class DownloadManager extends EventEmitter {
private storagePaths: StoragePaths;
public skipShutdownPersist = false;
private debridService: DebridService;
private invalidateMegaSessionFn?: () => void;
@ -2390,7 +2392,8 @@ export class DownloadManager extends EventEmitter {
const baseName = sanitizeFilename(parsed.name || "video");
let index = 1;
while (true) {
const MAX_ATTEMPTS = 10000;
while (index <= MAX_ATTEMPTS) {
const candidateName = index <= 1
? `${baseName}${extension}`
: `${baseName} (${index})${extension}`;
@ -2406,6 +2409,11 @@ export class DownloadManager extends EventEmitter {
}
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> {
@ -2821,6 +2829,9 @@ export class DownloadManager extends EventEmitter {
this.runCompletedPackages.clear();
this.retryAfterByItem.clear();
this.retryStateByItem.clear();
this.itemContributedBytes.clear();
this.reservedTargetPaths.clear();
this.claimedTargetPathByItem.clear();
this.session.running = true;
this.session.paused = false;
this.session.runStartedAt = nowMs();
@ -3047,6 +3058,9 @@ export class DownloadManager extends EventEmitter {
this.runCompletedPackages.clear();
this.retryAfterByItem.clear();
this.retryStateByItem.clear();
this.itemContributedBytes.clear();
this.reservedTargetPaths.clear();
this.claimedTargetPathByItem.clear();
this.session.running = true;
this.session.paused = false;
@ -3195,8 +3209,11 @@ export class DownloadManager extends EventEmitter {
this.nonResumableActive = 0;
this.session.summaryText = "";
// Persist synchronously on shutdown to guarantee data is written before process exits
// Skip if a backup was just imported — the restored session on disk must not be overwritten
if (!this.skipShutdownPersist) {
saveSession(this.storagePaths, this.session);
saveSettings(this.storagePaths, this.settings);
}
this.emitState(true);
logger.info(`Shutdown-Vorbereitung beendet: requeued=${requeuedItems}`);
}
@ -6056,9 +6073,17 @@ export class DownloadManager extends EventEmitter {
}
if (item.status === "completed" && item.targetPath) {
completedPaths.add(pathKey(item.targetPath));
} else if (item.targetPath) {
} else {
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) {
return ready;
@ -6408,13 +6433,21 @@ export class DownloadManager extends EventEmitter {
const errorText = String(error || "");
if (errorText.includes("aborted:extract")) {
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;
}
logger.warn(`Hybrid-Extract Fehler: pkg=${pkg.name}, reason=${compactErrorText(error)}`);
const errorAt = 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 - 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.updatedAt = errorAt;
}
@ -6892,6 +6925,7 @@ export class DownloadManager extends EventEmitter {
}
private finishRun(): void {
const runStartedAt = this.session.runStartedAt;
this.session.running = false;
this.session.paused = false;
this.session.runStartedAt = 0;
@ -6901,7 +6935,7 @@ export class DownloadManager extends EventEmitter {
const failed = outcomes.filter((status) => status === "failed").length;
const cancelled = outcomes.filter((status) => status === "cancelled").length;
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);
this.summary = {
total,

View File

@ -1637,6 +1637,8 @@ export function collectArchiveCleanupTargets(sourceArchivePath: string, director
if (multipartRar) {
const prefix = escapeRegex(multipartRar[1]);
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"));
return Array.from(targets);
}

View File

@ -424,6 +424,18 @@ export function normalizeLoadedSessionTransientFields(session: SessionState): Se
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;
}