Release v1.6.28

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-03-04 20:54:49 +01:00
parent 9a00304a93
commit 20a0a59670
6 changed files with 47 additions and 26 deletions

View File

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

@ -1051,7 +1051,11 @@ export class DebridService {
providerLabel: PROVIDER_LABELS[primary] providerLabel: PROVIDER_LABELS[primary]
}; };
} catch (error) { } catch (error) {
throw new Error(`Unrestrict fehlgeschlagen: ${PROVIDER_LABELS[primary]}: ${compactErrorText(error)}`); const errorText = compactErrorText(error);
if (signal?.aborted || (/aborted/i.test(errorText) && !/timeout/i.test(errorText))) {
throw error;
}
throw new Error(`Unrestrict fehlgeschlagen: ${PROVIDER_LABELS[primary]}: ${errorText}`);
} }
} }

View File

@ -1103,7 +1103,7 @@ export class DownloadManager extends EventEmitter {
if (pkg) { if (pkg) {
pkg.itemIds = pkg.itemIds.filter((id) => id !== itemId); pkg.itemIds = pkg.itemIds.filter((id) => id !== itemId);
if (pkg.itemIds.length === 0) { if (pkg.itemIds.length === 0) {
this.removePackageFromSession(item.packageId, []); this.removePackageFromSession(item.packageId, [itemId]);
} else { } else {
pkg.updatedAt = nowMs(); pkg.updatedAt = nowMs();
} }
@ -2659,6 +2659,17 @@ export class DownloadManager extends EventEmitter {
// 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)
for (const pkgId of affectedPackageIds) { for (const pkgId of affectedPackageIds) {
// Abort active post-processing for this package
const postProcessController = this.packagePostProcessAbortControllers.get(pkgId);
if (postProcessController && !postProcessController.signal.aborted) {
postProcessController.abort("reset");
}
this.packagePostProcessAbortControllers.delete(pkgId);
this.packagePostProcessTasks.delete(pkgId);
this.hybridExtractRequeue.delete(pkgId);
this.runCompletedPackages.delete(pkgId);
this.historyRecordedPackages.delete(pkgId);
const pkg = this.session.packages[pkgId]; const pkg = this.session.packages[pkgId];
if (pkg && (pkg.status === "completed" || pkg.status === "failed" || pkg.status === "cancelled")) { if (pkg && (pkg.status === "completed" || pkg.status === "failed" || pkg.status === "cancelled")) {
pkg.status = "queued"; pkg.status = "queued";
@ -3822,7 +3833,7 @@ export class DownloadManager extends EventEmitter {
continue; continue;
} }
if (this.settings.autoExtract && failed === 0 && cancelled === 0 && success > 0) { if (this.settings.autoExtract && failed === 0 && success > 0) {
const needsExtraction = items.some((item) => item.status === "completed" && !isExtractedLabel(item.fullStatus)); const needsExtraction = items.some((item) => item.status === "completed" && !isExtractedLabel(item.fullStatus));
if (needsExtraction) { if (needsExtraction) {
pkg.status = "queued"; pkg.status = "queued";
@ -3883,7 +3894,7 @@ export class DownloadManager extends EventEmitter {
const allDone = success + failed + cancelled >= items.length; const allDone = success + failed + cancelled >= items.length;
// Full extraction: all items done, no failures // Full extraction: all items done, no failures
if (allDone && failed === 0 && cancelled === 0 && success > 0) { if (allDone && failed === 0 && success > 0) {
const needsExtraction = items.some((item) => const needsExtraction = items.some((item) =>
item.status === "completed" && !isExtractedLabel(item.fullStatus) item.status === "completed" && !isExtractedLabel(item.fullStatus)
); );
@ -4944,22 +4955,8 @@ export class DownloadManager extends EventEmitter {
return; return;
} }
// Shelve check for non-stall errors
const totalNonStallFailures = (active.stallRetries || 0) + (active.unrestrictRetries || 0) + (active.genericErrorRetries || 0);
if (totalNonStallFailures >= 15) {
item.retries += 1;
active.stallRetries = Math.floor((active.stallRetries || 0) / 2);
active.unrestrictRetries = Math.floor((active.unrestrictRetries || 0) / 2);
active.genericErrorRetries = Math.floor((active.genericErrorRetries || 0) / 2);
logger.warn(`Item shelved (error path): ${item.fileName || item.id}, totalFailures=${totalNonStallFailures}, error=${errorText}`);
this.queueRetry(item, active, 300000, `Viele Fehler (${totalNonStallFailures}x), Pause 5 min`);
item.lastError = errorText;
this.persistSoon();
this.emitState();
return;
}
// Permanent link errors (dead link, file removed, hoster unavailable) → fail immediately // Permanent link errors (dead link, file removed, hoster unavailable) → fail immediately
// Check BEFORE shelve to avoid 5-min pause on dead links
if (isPermanentLinkError(errorText)) { if (isPermanentLinkError(errorText)) {
logger.error(`Link permanent ungültig: item=${item.fileName || item.id}, error=${errorText}, link=${item.url.slice(0, 80)}`); logger.error(`Link permanent ungültig: item=${item.fileName || item.id}, error=${errorText}, link=${item.url.slice(0, 80)}`);
item.status = "failed"; item.status = "failed";
@ -4974,6 +4971,21 @@ export class DownloadManager extends EventEmitter {
return; return;
} }
// Shelve check for non-stall errors (after permanent link error check)
const totalNonStallFailures = (active.stallRetries || 0) + (active.unrestrictRetries || 0) + (active.genericErrorRetries || 0);
if (totalNonStallFailures >= 15) {
item.retries += 1;
active.stallRetries = Math.floor((active.stallRetries || 0) / 2);
active.unrestrictRetries = Math.floor((active.unrestrictRetries || 0) / 2);
active.genericErrorRetries = Math.floor((active.genericErrorRetries || 0) / 2);
logger.warn(`Item shelved (error path): ${item.fileName || item.id}, totalFailures=${totalNonStallFailures}, error=${errorText}`);
this.queueRetry(item, active, 300000, `Viele Fehler (${totalNonStallFailures}x), Pause 5 min`);
item.lastError = errorText;
this.persistSoon();
this.emitState();
return;
}
if (isUnrestrictFailure(errorText) && active.unrestrictRetries < maxUnrestrictRetries) { if (isUnrestrictFailure(errorText) && active.unrestrictRetries < maxUnrestrictRetries) {
active.unrestrictRetries += 1; active.unrestrictRetries += 1;
item.retries += 1; item.retries += 1;
@ -5031,6 +5043,9 @@ export class DownloadManager extends EventEmitter {
} }
item.speedBps = 0; item.speedBps = 0;
item.updatedAt = nowMs(); item.updatedAt = nowMs();
// Refresh package status so it reflects "failed" when all items are done
const failPkg = this.session.packages[item.packageId];
if (failPkg) this.refreshPackageStatus(failPkg);
this.persistSoon(); this.persistSoon();
this.emitState(); this.emitState();
return; return;
@ -6379,7 +6394,7 @@ export class DownloadManager extends EventEmitter {
continue; continue;
} }
const status = entry.fullStatus || ""; const status = entry.fullStatus || "";
if (/^Entpacken\b/i.test(status)) { if (/^Entpacken\b/i.test(status) || /^Passwort\b/i.test(status)) {
if (result.failed > 0) { if (result.failed > 0) {
entry.fullStatus = "Entpacken - Error"; entry.fullStatus = "Entpacken - Error";
} else if (result.extracted > 0) { } else if (result.extracted > 0) {
@ -6399,7 +6414,7 @@ export class DownloadManager extends EventEmitter {
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 || "") || 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 - Ausstehend" || entry.fullStatus === "Entpacken - Warten auf Parts") {
entry.fullStatus = `Entpacken - Error`; entry.fullStatus = `Entpacken - Error`;
entry.updatedAt = errorAt; entry.updatedAt = errorAt;
} }
@ -6708,7 +6723,7 @@ export class DownloadManager extends EventEmitter {
timeoutHandled = true; timeoutHandled = true;
} else { } else {
for (const entry of completedItems) { for (const entry of completedItems) {
if (/^Entpacken/i.test(entry.fullStatus || "")) { if (/^Entpacken/i.test(entry.fullStatus || "") || /^Passwort/i.test(entry.fullStatus || "")) {
entry.fullStatus = "Entpacken abgebrochen (wird fortgesetzt)"; entry.fullStatus = "Entpacken abgebrochen (wird fortgesetzt)";
} }
entry.updatedAt = nowMs(); entry.updatedAt = nowMs();
@ -6879,6 +6894,7 @@ export class DownloadManager extends EventEmitter {
private finishRun(): void { private finishRun(): void {
this.session.running = false; this.session.running = false;
this.session.paused = false; this.session.paused = false;
this.session.runStartedAt = 0;
const total = this.runItemIds.size; const total = this.runItemIds.size;
const outcomes = Array.from(this.runOutcomes.values()); const outcomes = Array.from(this.runOutcomes.values());
const success = outcomes.filter((status) => status === "completed").length; const success = outcomes.filter((status) => status === "completed").length;

View File

@ -2006,6 +2006,7 @@ export async function extractPackageArchives(options: ExtractOptions): Promise<{
extracted += 1; extracted += 1;
resumeCompleted.add(archiveResumeKey); resumeCompleted.add(archiveResumeKey);
extractedArchives.add(archivePath); extractedArchives.add(archivePath);
await writeExtractResumeState(options.packageDir, resumeCompleted, options.packageId);
clearInterval(pulseTimer); clearInterval(pulseTimer);
return; return;
} }

View File

@ -295,7 +295,7 @@ function registerIpcHandlers(): void {
}); });
ipcMain.handle(IPC_CHANNELS.START_ITEMS, (_event: IpcMainInvokeEvent, itemIds: string[]) => { ipcMain.handle(IPC_CHANNELS.START_ITEMS, (_event: IpcMainInvokeEvent, itemIds: string[]) => {
validateStringArray(itemIds ?? [], "itemIds"); validateStringArray(itemIds ?? [], "itemIds");
return controller.startItems(itemIds); return controller.startItems(itemIds ?? []);
}); });
ipcMain.handle(IPC_CHANNELS.STOP, () => controller.stop()); ipcMain.handle(IPC_CHANNELS.STOP, () => controller.stop());
ipcMain.handle(IPC_CHANNELS.TOGGLE_PAUSE, () => controller.togglePause()); ipcMain.handle(IPC_CHANNELS.TOGGLE_PAUSE, () => controller.togglePause());

View File

@ -360,8 +360,8 @@ function sortPackageOrderBySize(order: string[], packages: Record<string, Packag
function sortPackageOrderByHoster(order: string[], packages: Record<string, PackageEntry>, items: Record<string, DownloadItem>, descending: boolean): string[] { function sortPackageOrderByHoster(order: string[], packages: Record<string, PackageEntry>, items: Record<string, DownloadItem>, descending: boolean): string[] {
const sorted = [...order]; const sorted = [...order];
sorted.sort((a, b) => { sorted.sort((a, b) => {
const hosterA = [...new Set((packages[a]?.itemIds ?? []).map((id) => items[id]?.provider).filter(Boolean))].join(",").toLowerCase(); const hosterA = [...new Set((packages[a]?.itemIds ?? []).map((id) => extractHoster(items[id]?.url ?? "")).filter(Boolean))].join(",").toLowerCase();
const hosterB = [...new Set((packages[b]?.itemIds ?? []).map((id) => items[id]?.provider).filter(Boolean))].join(",").toLowerCase(); const hosterB = [...new Set((packages[b]?.itemIds ?? []).map((id) => extractHoster(items[id]?.url ?? "")).filter(Boolean))].join(",").toLowerCase();
const cmp = hosterA.localeCompare(hosterB); const cmp = hosterA.localeCompare(hosterB);
return descending ? -cmp : cmp; return descending ? -cmp : cmp;
}); });