Release v1.5.95

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-03-04 04:08:29 +01:00
parent 4fcbd5c4f7
commit 00fae5cadd
5 changed files with 48 additions and 30 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.5.94", "version": "1.5.95",
"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 = 2; export const SPEED_WINDOW_SECONDS = 1;
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

@ -22,7 +22,7 @@ import {
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 { 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 { clearExtractResumeState, collectArchiveCleanupTargets, extractPackageArchives, findArchiveCandidates } from "./extractor";
import { validateFileAgainstManifest } from "./integrity"; import { validateFileAgainstManifest } from "./integrity";
import { logger } from "./logger"; import { logger } from "./logger";
import { StoragePaths, saveSession, saveSessionAsync, saveSettings, saveSettingsAsync } from "./storage"; import { StoragePaths, saveSession, saveSessionAsync, saveSettings, saveSettingsAsync } from "./storage";
@ -2476,7 +2476,13 @@ export class DownloadManager extends EventEmitter {
postProcessController.abort("reset"); postProcessController.abort("reset");
} }
// 3. Reset package state // 3. Clean up extraction progress manifest (.rd_extract_progress.json)
if (pkg.outputDir) {
clearExtractResumeState(pkg.outputDir, packageId).catch(() => {});
clearExtractResumeState(pkg.outputDir).catch(() => {});
}
// 4. Reset package state
pkg.status = "queued"; pkg.status = "queued";
pkg.cancelled = false; pkg.cancelled = false;
pkg.enabled = true; pkg.enabled = true;
@ -3305,13 +3311,13 @@ 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
? 900 ? 700
: itemCount >= 700 : itemCount >= 700
? 650 ? 500
: itemCount >= 250 : itemCount >= 250
? 400 ? 300
: 250 : 150
: 260; : 200;
this.stateEmitTimer = setTimeout(() => { this.stateEmitTimer = setTimeout(() => {
this.stateEmitTimer = null; this.stateEmitTimer = null;
this.lastStateEmitAt = nowMs(); this.lastStateEmitAt = nowMs();
@ -3729,8 +3735,15 @@ export class DownloadManager extends EventEmitter {
delete this.session.packages[packageId]; delete this.session.packages[packageId];
this.session.packageOrder = this.session.packageOrder.filter((id) => id !== packageId); this.session.packageOrder = this.session.packageOrder.filter((id) => id !== packageId);
// Keep packageId in runPackageIds so the "size > 0" guard still filters // Keep packageId in runPackageIds so the "size > 0" guard still filters
// other packages. The deleted package has no items left, so the scheduler // other packages. But prune ghost entries: if no real package remains in
// simply won't find anything for it. finishRun() clears runPackageIds. // the set, clear it so the scheduler isn't permanently blocked.
if (this.runPackageIds.size > 0) {
for (const rpId of this.runPackageIds) {
if (!this.session.packages[rpId]) {
this.runPackageIds.delete(rpId);
}
}
}
this.runCompletedPackages.delete(packageId); this.runCompletedPackages.delete(packageId);
this.hybridExtractRequeue.delete(packageId); this.hybridExtractRequeue.delete(packageId);
this.resetSessionTotalsIfQueueEmpty(); this.resetSessionTotalsIfQueueEmpty();
@ -4920,12 +4933,12 @@ export class DownloadManager extends EventEmitter {
let windowStarted = nowMs(); let windowStarted = nowMs();
const itemCount = this.itemCount; const itemCount = this.itemCount;
const uiUpdateIntervalMs = itemCount >= 1500 const uiUpdateIntervalMs = itemCount >= 1500
? 650 ? 500
: itemCount >= 700 : itemCount >= 700
? 420 ? 350
: itemCount >= 250 : itemCount >= 250
? 280 ? 220
: 170; : 120;
let lastUiEmitAt = 0; let lastUiEmitAt = 0;
const stallTimeoutMs = getDownloadStallTimeoutMs(); const stallTimeoutMs = getDownloadStallTimeoutMs();
const drainTimeoutMs = Math.max(30000, Math.min(300000, stallTimeoutMs > 0 ? stallTimeoutMs * 12 : 120000)); const drainTimeoutMs = Math.max(30000, Math.min(300000, stallTimeoutMs > 0 ? stallTimeoutMs * 12 : 120000));
@ -5187,9 +5200,9 @@ export class DownloadManager extends EventEmitter {
throughputWindowBytes = 0; throughputWindowBytes = 0;
} }
const elapsed = Math.max((nowMs() - windowStarted) / 1000, 0.3); const elapsed = Math.max((nowMs() - windowStarted) / 1000, 0.2);
const speed = windowBytes / elapsed; const speed = windowBytes / elapsed;
if (elapsed >= 0.8) { if (elapsed >= 0.5) {
windowStarted = nowMs(); windowStarted = nowMs();
windowBytes = 0; windowBytes = 0;
} }
@ -5882,13 +5895,12 @@ export class DownloadManager extends EventEmitter {
activeHybridArchiveMap.delete(progress.archiveName); activeHybridArchiveMap.delete(progress.archiveName);
hybridArchiveStartTimes.delete(progress.archiveName); hybridArchiveStartTimes.delete(progress.archiveName);
} else { } else {
// Update this archive's items with current progress // Update this archive's items with per-archive progress
const archive = ` · ${progress.archiveName}`; const archiveLabel = ` · ${progress.archiveName}`;
const elapsed = progress.elapsedMs && progress.elapsedMs >= 1000 const elapsed = progress.elapsedMs && progress.elapsedMs >= 1000
? ` · ${Math.floor(progress.elapsedMs / 1000)}s` ? ` · ${Math.floor(progress.elapsedMs / 1000)}s`
: ""; : "";
const activeArchive = Number(progress.archivePercent ?? 0) > 0 ? 1 : 0; const archivePct = Math.max(0, Math.min(100, Math.floor(Number(progress.archivePercent ?? 0))));
const currentDisplay = Math.max(0, Math.min(progress.total, progress.current + activeArchive));
let label: string; let label: string;
if (progress.passwordFound) { if (progress.passwordFound) {
label = `Passwort gefunden · ${progress.archiveName}`; label = `Passwort gefunden · ${progress.archiveName}`;
@ -5896,7 +5908,7 @@ export class DownloadManager extends EventEmitter {
const pwPct = Math.round((progress.passwordAttempt / progress.passwordTotal) * 100); const pwPct = Math.round((progress.passwordAttempt / progress.passwordTotal) * 100);
label = `Passwort knacken: ${pwPct}% (${progress.passwordAttempt}/${progress.passwordTotal}) · ${progress.archiveName}`; label = `Passwort knacken: ${pwPct}% (${progress.passwordAttempt}/${progress.passwordTotal}) · ${progress.archiveName}`;
} else { } else {
label = `Entpacken ${progress.percent}% (${currentDisplay}/${progress.total})${archive}${elapsed}`; label = `Entpacken ${archivePct}%${archiveLabel}${elapsed}`;
} }
const updatedAt = nowMs(); const updatedAt = nowMs();
for (const entry of archItems) { for (const entry of archItems) {
@ -5908,10 +5920,17 @@ export class DownloadManager extends EventEmitter {
} }
} }
// Throttled emit // Throttled emit — also promote "Warten auf Parts" items that
// completed downloading in the meantime to "Ausstehend".
const now = nowMs(); const now = nowMs();
if (now - hybridLastEmitAt >= EXTRACT_PROGRESS_EMIT_INTERVAL_MS) { if (now - hybridLastEmitAt >= EXTRACT_PROGRESS_EMIT_INTERVAL_MS) {
hybridLastEmitAt = now; hybridLastEmitAt = now;
for (const entry of items) {
if (entry.status === "completed" && entry.fullStatus === "Entpacken - Warten auf Parts") {
entry.fullStatus = "Entpacken - Ausstehend";
entry.updatedAt = now;
}
}
this.emitState(); this.emitState();
} }
} }
@ -6144,13 +6163,12 @@ export class DownloadManager extends EventEmitter {
activeArchiveItemsMap.delete(progress.archiveName); activeArchiveItemsMap.delete(progress.archiveName);
archiveStartTimes.delete(progress.archiveName); archiveStartTimes.delete(progress.archiveName);
} else { } else {
// Update this archive's items with current progress // Update this archive's items with per-archive progress
const archive = progress.archiveName ? ` · ${progress.archiveName}` : ""; const archiveTag = progress.archiveName ? ` · ${progress.archiveName}` : "";
const elapsed = progress.elapsedMs && progress.elapsedMs >= 1000 const elapsed = progress.elapsedMs && progress.elapsedMs >= 1000
? ` · ${Math.floor(progress.elapsedMs / 1000)}s` ? ` · ${Math.floor(progress.elapsedMs / 1000)}s`
: ""; : "";
const activeArchive = Number(progress.archivePercent ?? 0) > 0 ? 1 : 0; const archivePct = Math.max(0, Math.min(100, Math.floor(Number(progress.archivePercent ?? 0))));
const currentDisplay = Math.max(0, Math.min(progress.total, progress.current + activeArchive));
let label: string; let label: string;
if (progress.passwordFound) { if (progress.passwordFound) {
label = `Passwort gefunden · ${progress.archiveName}`; label = `Passwort gefunden · ${progress.archiveName}`;
@ -6158,7 +6176,7 @@ export class DownloadManager extends EventEmitter {
const pwPct = Math.round((progress.passwordAttempt / progress.passwordTotal) * 100); const pwPct = Math.round((progress.passwordAttempt / progress.passwordTotal) * 100);
label = `Passwort knacken: ${pwPct}% (${progress.passwordAttempt}/${progress.passwordTotal}) · ${progress.archiveName}`; label = `Passwort knacken: ${pwPct}% (${progress.passwordAttempt}/${progress.passwordTotal}) · ${progress.archiveName}`;
} else { } else {
label = `Entpacken ${progress.percent}% (${currentDisplay}/${progress.total})${archive}${elapsed}`; label = `Entpacken ${archivePct}%${archiveTag}${elapsed}`;
} }
const updatedAt = nowMs(); const updatedAt = nowMs();
for (const entry of archiveItems) { for (const entry of archiveItems) {

View File

@ -432,7 +432,7 @@ async function writeExtractResumeState(packageDir: string, completedArchives: Se
} }
} }
async function clearExtractResumeState(packageDir: string, packageId?: string): Promise<void> { export async function clearExtractResumeState(packageDir: string, packageId?: string): Promise<void> {
try { try {
await fs.promises.rm(extractProgressFilePath(packageDir, packageId), { force: true }); await fs.promises.rm(extractProgressFilePath(packageDir, packageId), { force: true });
} catch { } catch {

View File

@ -3082,7 +3082,7 @@ const PackageCard = memo(function PackageCard({ pkg, items, packageSpeed, isFirs
// (prevents bar jumping from 100% to 50% when extraction starts) // (prevents bar jumping from 100% to 50% when extraction starts)
const allDownloaded = done + failed + cancelled >= total; const allDownloaded = done + failed + cancelled >= total;
const allExtracted = extracted >= total; const allExtracted = extracted >= total;
const useExtractSplit = extracting || pkg.status === "extracting" || (allDownloaded && !allExtracted && done > 0 && extracted > 0); const useExtractSplit = extracting || pkg.status === "extracting" || (allDownloaded && !allExtracted && done > 0 && extracted > 0 && failed === 0 && cancelled === 0);
// Include fractional progress from active downloads so the bar moves continuously // Include fractional progress from active downloads so the bar moves continuously
const activeProgress = items.reduce((sum, item) => { const activeProgress = items.reduce((sum, item) => {
if (item.status === "downloading" || (item.status === "queued" && (item.progressPercent || 0) > 0)) { if (item.status === "downloading" || (item.status === "queued" && (item.progressPercent || 0) > 0)) {