Release v1.5.91

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-03-04 03:15:54 +01:00
parent 254612a49b
commit 818bf40a9c
3 changed files with 82 additions and 18 deletions

View File

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

@ -31,7 +31,7 @@ export const RAR_SPLIT_RE = /\.r\d{2,3}$/i;
export const MAX_MANIFEST_FILE_BYTES = 5 * 1024 * 1024; export const MAX_MANIFEST_FILE_BYTES = 5 * 1024 * 1024;
export const MAX_LINK_ARTIFACT_BYTES = 256 * 1024; export const MAX_LINK_ARTIFACT_BYTES = 256 * 1024;
export const SPEED_WINDOW_SECONDS = 3; export const SPEED_WINDOW_SECONDS = 2;
export const CLIPBOARD_POLL_INTERVAL_MS = 2000; export const CLIPBOARD_POLL_INTERVAL_MS = 2000;
export const DEFAULT_UPDATE_REPO = "Sucukdeluxe/real-debrid-downloader"; export const DEFAULT_UPDATE_REPO = "Sucukdeluxe/real-debrid-downloader";

View File

@ -19,7 +19,7 @@ import {
StartConflictResolutionResult, StartConflictResolutionResult,
UiSnapshot UiSnapshot
} from "../shared/types"; } from "../shared/types";
import { REQUEST_RETRIES, SAMPLE_VIDEO_EXTENSIONS, WRITE_BUFFER_SIZE, WRITE_FLUSH_TIMEOUT_MS, ALLOCATION_UNIT_SIZE, STREAM_HIGH_WATER_MARK } from "./constants"; import { REQUEST_RETRIES, SAMPLE_VIDEO_EXTENSIONS, SPEED_WINDOW_SECONDS, WRITE_BUFFER_SIZE, WRITE_FLUSH_TIMEOUT_MS, ALLOCATION_UNIT_SIZE, STREAM_HIGH_WATER_MARK } from "./constants";
import { cleanupCancelledPackageArtifactsAsync } from "./cleanup"; import { cleanupCancelledPackageArtifactsAsync } from "./cleanup";
import { DebridService, MegaWebUnrestrictor, checkRapidgatorOnline } from "./debrid"; import { DebridService, MegaWebUnrestrictor, checkRapidgatorOnline } from "./debrid";
import { collectArchiveCleanupTargets, extractPackageArchives, findArchiveCandidates } from "./extractor"; import { collectArchiveCleanupTargets, extractPackageArchives, findArchiveCandidates } from "./extractor";
@ -852,11 +852,15 @@ export class DownloadManager extends EventEmitter {
} }
public setSettings(next: AppSettings): void { public setSettings(next: AppSettings): void {
const prevCleanupPolicy = this.settings.completedCleanupPolicy;
next.totalDownloadedAllTime = Math.max(next.totalDownloadedAllTime || 0, this.settings.totalDownloadedAllTime || 0); next.totalDownloadedAllTime = Math.max(next.totalDownloadedAllTime || 0, this.settings.totalDownloadedAllTime || 0);
this.settings = next; this.settings = next;
this.debridService.setSettings(next); this.debridService.setSettings(next);
this.resolveExistingQueuedOpaqueFilenames(); this.resolveExistingQueuedOpaqueFilenames();
void this.cleanupExistingExtractedArchives().catch((err) => logger.warn(`cleanupExistingExtractedArchives Fehler (setSettings): ${compactErrorText(err)}`)); void this.cleanupExistingExtractedArchives().catch((err) => logger.warn(`cleanupExistingExtractedArchives Fehler (setSettings): ${compactErrorText(err)}`));
if (prevCleanupPolicy !== next.completedCleanupPolicy && next.completedCleanupPolicy !== "never") {
this.applyRetroactiveCleanupPolicy();
}
this.emitState(); this.emitState();
} }
@ -876,7 +880,7 @@ export class DownloadManager extends EventEmitter {
const now = nowMs(); const now = nowMs();
this.pruneSpeedEvents(now); this.pruneSpeedEvents(now);
const paused = this.session.running && this.session.paused; const paused = this.session.running && this.session.paused;
const speedBps = paused ? 0 : this.speedBytesLastWindow / 3; const speedBps = !this.session.running || paused ? 0 : this.speedBytesLastWindow / SPEED_WINDOW_SECONDS;
let totalItems = 0; let totalItems = 0;
let doneItems = 0; let doneItems = 0;
@ -926,8 +930,8 @@ export class DownloadManager extends EventEmitter {
canPause: this.session.running, canPause: this.session.running,
clipboardActive: this.settings.clipboardWatch, clipboardActive: this.settings.clipboardWatch,
reconnectSeconds: Math.ceil(reconnectMs / 1000), reconnectSeconds: Math.ceil(reconnectMs / 1000),
packageSpeedBps: paused ? {} : Object.fromEntries( packageSpeedBps: !this.session.running || paused ? {} : Object.fromEntries(
[...this.speedBytesPerPackage].map(([pid, bytes]) => [pid, Math.floor(bytes / 3)]) [...this.speedBytesPerPackage].map(([pid, bytes]) => [pid, Math.floor(bytes / SPEED_WINDOW_SECONDS)])
) )
}; };
} }
@ -2863,6 +2867,10 @@ export class DownloadManager extends EventEmitter {
this.retryAfterByItem.clear(); this.retryAfterByItem.clear();
this.lastGlobalProgressBytes = this.session.totalDownloadedBytes; this.lastGlobalProgressBytes = this.session.totalDownloadedBytes;
this.lastGlobalProgressAt = nowMs(); this.lastGlobalProgressAt = nowMs();
this.speedEvents = [];
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.speedEventsHead = 0;
this.abortPostProcessing("stop"); this.abortPostProcessing("stop");
for (const active of this.activeTasks.values()) { for (const active of this.activeTasks.values()) {
active.abortReason = "stop"; active.abortReason = "stop";
@ -2970,6 +2978,10 @@ export class DownloadManager extends EventEmitter {
// When pausing: abort active extractions so they don't continue during pause // When pausing: abort active extractions so they don't continue during pause
if (!wasPaused && this.session.paused) { if (!wasPaused && this.session.paused) {
this.abortPostProcessing("pause"); this.abortPostProcessing("pause");
this.speedEvents = [];
this.speedBytesLastWindow = 0;
this.speedBytesPerPackage.clear();
this.speedEventsHead = 0;
} }
// When unpausing: clear all retry delays so stuck queued items restart immediately, // When unpausing: clear all retry delays so stuck queued items restart immediately,
@ -3127,6 +3139,57 @@ export class DownloadManager extends EventEmitter {
} }
} }
private applyRetroactiveCleanupPolicy(): void {
const policy = this.settings.completedCleanupPolicy;
if (policy === "never") return;
let removed = 0;
for (const pkgId of [...this.session.packageOrder]) {
const pkg = this.session.packages[pkgId];
if (!pkg) continue;
if (policy === "immediate") {
const completedItemIds = pkg.itemIds.filter((itemId) => {
const item = this.session.items[itemId];
if (!item || item.status !== "completed") return false;
if (this.settings.autoExtract) return isExtractedLabel(item.fullStatus || "");
return true;
});
for (const itemId of completedItemIds) {
pkg.itemIds = pkg.itemIds.filter((id) => id !== itemId);
this.releaseTargetPath(itemId);
this.dropItemContribution(itemId);
delete this.session.items[itemId];
this.itemCount = Math.max(0, this.itemCount - 1);
this.retryAfterByItem.delete(itemId);
removed += 1;
}
if (pkg.itemIds.length === 0) {
this.removePackageFromSession(pkgId, []);
}
} else if (policy === "package_done" || policy === "on_start") {
const allCompleted = pkg.itemIds.every((id) => {
const item = this.session.items[id];
return !item || item.status === "completed";
});
if (!allCompleted) continue;
if (this.settings.autoExtract) {
const allExtracted = pkg.itemIds.every((id) => {
const item = this.session.items[id];
return !item || isExtractedLabel(item.fullStatus || "");
});
if (!allExtracted) continue;
}
removed += pkg.itemIds.length;
this.removePackageFromSession(pkgId, [...pkg.itemIds], "completed");
}
}
if (removed > 0) {
logger.info(`Retroaktive Bereinigung: ${removed} fertige Items entfernt (policy=${policy})`);
this.persistSoon();
}
}
private clearPersistTimer(): void { private clearPersistTimer(): void {
if (!this.persistTimer) { if (!this.persistTimer) {
return; return;
@ -3199,12 +3262,12 @@ export class DownloadManager extends EventEmitter {
const itemCount = this.itemCount; const itemCount = this.itemCount;
const emitDelay = this.session.running const emitDelay = this.session.running
? itemCount >= 1500 ? itemCount >= 1500
? 1200 ? 900
: itemCount >= 700 : itemCount >= 700
? 900 ? 650
: itemCount >= 250 : itemCount >= 250
? 560 ? 400
: 320 : 250
: 260; : 260;
this.stateEmitTimer = setTimeout(() => { this.stateEmitTimer = setTimeout(() => {
this.stateEmitTimer = null; this.stateEmitTimer = null;
@ -3217,7 +3280,7 @@ export class DownloadManager extends EventEmitter {
private speedBytesPerPackage = new Map<string, number>(); private speedBytesPerPackage = new Map<string, number>();
private pruneSpeedEvents(now: number): void { private pruneSpeedEvents(now: number): void {
const cutoff = now - 3000; const cutoff = now - SPEED_WINDOW_SECONDS * 1000;
while (this.speedEventsHead < this.speedEvents.length && this.speedEvents[this.speedEventsHead].at < cutoff) { while (this.speedEventsHead < this.speedEvents.length && this.speedEvents[this.speedEventsHead].at < cutoff) {
const ev = this.speedEvents[this.speedEventsHead]; const ev = this.speedEvents[this.speedEventsHead];
this.speedBytesLastWindow = Math.max(0, this.speedBytesLastWindow - ev.bytes); this.speedBytesLastWindow = Math.max(0, this.speedBytesLastWindow - ev.bytes);
@ -3458,9 +3521,8 @@ export class DownloadManager extends EventEmitter {
} }
if (this.settings.autoExtract && failed === 0 && success > 0) { if (this.settings.autoExtract && failed === 0 && success > 0) {
const needsPostProcess = pkg.status !== "completed" const needsExtraction = items.some((item) => item.status === "completed" && !isExtractedLabel(item.fullStatus));
|| items.some((item) => item.status === "completed" && !isExtractedLabel(item.fullStatus)); if (needsExtraction) {
if (needsPostProcess) {
pkg.status = "queued"; pkg.status = "queued";
pkg.updatedAt = nowMs(); pkg.updatedAt = nowMs();
for (const item of items) { for (const item of items) {
@ -5080,9 +5142,9 @@ export class DownloadManager extends EventEmitter {
throughputWindowBytes = 0; throughputWindowBytes = 0;
} }
const elapsed = Math.max((nowMs() - windowStarted) / 1000, 0.5); const elapsed = Math.max((nowMs() - windowStarted) / 1000, 0.3);
const speed = windowBytes / elapsed; const speed = windowBytes / elapsed;
if (elapsed >= 1.2) { if (elapsed >= 0.8) {
windowStarted = nowMs(); windowStarted = nowMs();
windowBytes = 0; windowBytes = 0;
} }
@ -5659,6 +5721,7 @@ export class DownloadManager extends EventEmitter {
logger.info(`Hybrid-Extract Start: pkg=${pkg.name}, readyArchives=${readyArchives.size}`); logger.info(`Hybrid-Extract Start: pkg=${pkg.name}, readyArchives=${readyArchives.size}`);
pkg.status = "extracting"; pkg.status = "extracting";
this.emitState(); this.emitState();
const hybridExtractStartMs = nowMs();
const completedItems = items.filter((item) => item.status === "completed"); const completedItems = items.filter((item) => item.status === "completed");
@ -5825,7 +5888,7 @@ export class DownloadManager extends EventEmitter {
const status = entry.fullStatus || ""; const status = entry.fullStatus || "";
if (/^Entpacken\b/i.test(status) || /^Fertig\b/i.test(status)) { if (/^Entpacken\b/i.test(status) || /^Fertig\b/i.test(status)) {
if (result.extracted > 0 && result.failed === 0) { if (result.extracted > 0 && result.failed === 0) {
entry.fullStatus = "Entpackt - Done"; entry.fullStatus = formatExtractDone(nowMs() - hybridExtractStartMs);
} else { } else {
entry.fullStatus = "Entpacken - Error"; entry.fullStatus = "Entpacken - Error";
} }
@ -5920,6 +5983,7 @@ export class DownloadManager extends EventEmitter {
if (this.settings.autoExtract && failed === 0 && success > 0 && !alreadyMarkedExtracted) { if (this.settings.autoExtract && failed === 0 && success > 0 && !alreadyMarkedExtracted) {
pkg.status = "extracting"; pkg.status = "extracting";
this.emitState(); this.emitState();
const extractionStartMs = nowMs();
const resolveArchiveItems = (archiveName: string): DownloadItem[] => const resolveArchiveItems = (archiveName: string): DownloadItem[] =>
resolveArchiveItemsFromList(archiveName, completedItems); resolveArchiveItemsFromList(archiveName, completedItems);
@ -6094,7 +6158,7 @@ export class DownloadManager extends EventEmitter {
let finalStatusText = ""; let finalStatusText = "";
if (result.extracted > 0 || hasExtractedOutput) { if (result.extracted > 0 || hasExtractedOutput) {
finalStatusText = "Entpackt - Done"; finalStatusText = formatExtractDone(nowMs() - extractionStartMs);
} else if (!sourceExists) { } else if (!sourceExists) {
finalStatusText = "Entpackt (Quelle fehlt)"; finalStatusText = "Entpackt (Quelle fehlt)";
logger.warn(`Post-Processing ohne Quellordner: pkg=${pkg.name}, outputDir fehlt`); logger.warn(`Post-Processing ohne Quellordner: pkg=${pkg.name}, outputDir fehlt`);