Compare commits

..

No commits in common. "c1edb070099c1fcde3a0ea5a305c003e14f72ca3" and "26edc797841610f5ac24688f7047e3cf53c9bbc6" have entirely different histories.

4 changed files with 15 additions and 86 deletions

View File

@ -1,6 +1,6 @@
{ {
"name": "real-debrid-downloader", "name": "real-debrid-downloader",
"version": "1.7.134", "version": "1.7.133",
"description": "Desktop downloader", "description": "Desktop downloader",
"main": "build/main/main/main.js", "main": "build/main/main/main.js",
"author": "Sucukdeluxe", "author": "Sucukdeluxe",

View File

@ -59,28 +59,6 @@ export function resetDebridLinkRuntimeStateForTests(): void {
debridLinkKeyRuntimeStatuses.clear(); debridLinkKeyRuntimeStatuses.clear();
} }
/** Periodic cleanup of expired Debrid-Link cooldown/runtime entries.
* Without this, module-level Maps grow unbounded over 24/7 operation.
* Removes entries whose cooldown expired more than 1 hour ago. */
export function pruneExpiredDebridLinkRuntimeState(now = Date.now()): number {
let removed = 0;
const grace = 60 * 60 * 1000; // keep 1h grace for debugging
for (const [keyId, until] of debridLinkKeyCooldowns) {
if (until + grace < now) {
debridLinkKeyCooldowns.delete(keyId);
debridLinkKeyCooldownDetails.delete(keyId);
removed += 1;
}
}
for (const [keyId, status] of debridLinkKeyRuntimeStatuses) {
if (!debridLinkKeyCooldowns.has(keyId) && now - status.updatedAt > grace) {
debridLinkKeyRuntimeStatuses.delete(keyId);
removed += 1;
}
}
return removed;
}
export function primeDebridLinkRuntimeCooldownForTests(keyId: string, cooldownMs: number, message = "Debrid-Link Key im Cooldown"): void { export function primeDebridLinkRuntimeCooldownForTests(keyId: string, cooldownMs: number, message = "Debrid-Link Key im Cooldown"): void {
setDebridLinkKeyCooldownState(keyId, cooldownMs, message, "temporary"); setDebridLinkKeyCooldownState(keyId, cooldownMs, message, "temporary");
} }
@ -162,19 +140,6 @@ export function resetMegaDebridRuntimeStateForTests(): void {
megaDebridAccountCooldowns.clear(); megaDebridAccountCooldowns.clear();
} }
/** Periodic cleanup of expired Mega-Debrid cooldown entries. */
export function pruneExpiredMegaDebridRuntimeState(now = Date.now()): number {
let removed = 0;
const grace = 60 * 60 * 1000;
for (const [id, detail] of megaDebridAccountCooldowns) {
if (detail.until + grace < now) {
megaDebridAccountCooldowns.delete(id);
removed += 1;
}
}
return removed;
}
export function primeMegaDebridRuntimeCooldownForTests(accountId: string, cooldownMs: number, message = "Mega-Debrid Account im Cooldown"): void { export function primeMegaDebridRuntimeCooldownForTests(accountId: string, cooldownMs: number, message = "Mega-Debrid Account im Cooldown"): void {
setMegaDebridAccountCooldownState(accountId, cooldownMs, message, "temporary"); setMegaDebridAccountCooldownState(accountId, cooldownMs, message, "temporary");
} }

View File

@ -51,7 +51,7 @@ function releaseTlsSkip(): void {
} }
import { cleanupCancelledPackageArtifactsAsync, removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup"; import { cleanupCancelledPackageArtifactsAsync, removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup";
import { planDownloadCompletion, validateDownloadedFileCompletion } from "./download-completion"; import { planDownloadCompletion, validateDownloadedFileCompletion } from "./download-completion";
import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkRapidgatorOnline, fetchAllDebridHostInfo, getAvailableDebridLinkApiKeys, pruneExpiredDebridLinkRuntimeState, pruneExpiredMegaDebridRuntimeState } from "./debrid"; import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkRapidgatorOnline, fetchAllDebridHostInfo, getAvailableDebridLinkApiKeys } from "./debrid";
import { cleanupArchives, clearExtractResumeState, collectArchiveCleanupTargets, detectArchiveSignature, extractPackageArchives, findArchiveCandidates, hasAnyFilesRecursive, removeEmptyDirectoryTree, type ExtractArchiveFailureInfo } from "./extractor"; import { cleanupArchives, clearExtractResumeState, collectArchiveCleanupTargets, detectArchiveSignature, extractPackageArchives, findArchiveCandidates, hasAnyFilesRecursive, removeEmptyDirectoryTree, type ExtractArchiveFailureInfo } from "./extractor";
import { validateFileAgainstManifest } from "./integrity"; import { validateFileAgainstManifest } from "./integrity";
import { logger } from "./logger"; import { logger } from "./logger";
@ -1325,15 +1325,6 @@ export function buildAutoRenameBaseNameFromFoldersWithOptions(
return null; return null;
} }
// Hoisted regex patterns — avoid recompiling on every resolveArchiveItemsFromList() call.
const ARCHIVE_MULTIPART_RAR_RE = /^(.*)\.part0*1\.rar$/;
const ARCHIVE_RAR_RE = /^(.*)\.rar$/;
const ARCHIVE_ZIP_SPLIT_RE = /^(.*)\.zip\.001$/;
const ARCHIVE_7Z_SPLIT_RE = /^(.*)\.7z\.001$/;
const ARCHIVE_GENERIC_001_RE = /^(.*)\.001$/;
const ARCHIVE_KNOWN_001_RE = /\.(zip|7z)\.001$/;
const REGEX_ESCAPE_RE = /[.*+?^${}()|[\]\\]/g;
export function resolveArchiveItemsFromList(archiveName: string, items: DownloadItem[]): DownloadItem[] { export function resolveArchiveItemsFromList(archiveName: string, items: DownloadItem[]): DownloadItem[] {
const normalizeArchiveMatchName = (value: string): string => const normalizeArchiveMatchName = (value: string): string =>
stripDuplicateSuffixBeforeExtension(path.basename(String(value || ""))); stripDuplicateSuffixBeforeExtension(path.basename(String(value || "")));
@ -1343,40 +1334,38 @@ export function resolveArchiveItemsFromList(archiveName: string, items: Download
const itemBaseName = (item: DownloadItem): string => const itemBaseName = (item: DownloadItem): string =>
normalizeArchiveMatchName(item.targetPath || item.fileName || ""); normalizeArchiveMatchName(item.targetPath || item.fileName || "");
// Try pattern-based matching first (for multipart archives). // Try pattern-based matching first (for multipart archives)
// Note: the constructed RegExps below depend on the input filename so they
// cannot be hoisted — but the *test* regexes above are now reused.
let pattern: RegExp | null = null; let pattern: RegExp | null = null;
const multipartMatch = entryLower.match(ARCHIVE_MULTIPART_RAR_RE); const multipartMatch = entryLower.match(/^(.*)\.part0*1\.rar$/);
if (multipartMatch) { if (multipartMatch) {
const prefix = multipartMatch[1].replace(REGEX_ESCAPE_RE, "\\$&"); const prefix = multipartMatch[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
pattern = new RegExp(`^${prefix}\\.part\\d+\\.rar$`, "i"); pattern = new RegExp(`^${prefix}\\.part\\d+\\.rar$`, "i");
} }
if (!pattern) { if (!pattern) {
const rarMatch = entryLower.match(ARCHIVE_RAR_RE); const rarMatch = entryLower.match(/^(.*)\.rar$/);
if (rarMatch) { if (rarMatch) {
const stem = rarMatch[1].replace(REGEX_ESCAPE_RE, "\\$&"); const stem = rarMatch[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
pattern = new RegExp(`^${stem}\\.r(ar|\\d{2,3})$`, "i"); pattern = new RegExp(`^${stem}\\.r(ar|\\d{2,3})$`, "i");
} }
} }
if (!pattern) { if (!pattern) {
const zipSplitMatch = entryLower.match(ARCHIVE_ZIP_SPLIT_RE); const zipSplitMatch = entryLower.match(/^(.*)\.zip\.001$/);
if (zipSplitMatch) { if (zipSplitMatch) {
const stem = zipSplitMatch[1].replace(REGEX_ESCAPE_RE, "\\$&"); const stem = zipSplitMatch[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
pattern = new RegExp(`^${stem}\\.zip(\\.\\d+)?$`, "i"); pattern = new RegExp(`^${stem}\\.zip(\\.\\d+)?$`, "i");
} }
} }
if (!pattern) { if (!pattern) {
const sevenSplitMatch = entryLower.match(ARCHIVE_7Z_SPLIT_RE); const sevenSplitMatch = entryLower.match(/^(.*)\.7z\.001$/);
if (sevenSplitMatch) { if (sevenSplitMatch) {
const stem = sevenSplitMatch[1].replace(REGEX_ESCAPE_RE, "\\$&"); const stem = sevenSplitMatch[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
pattern = new RegExp(`^${stem}\\.7z(\\.\\d+)?$`, "i"); pattern = new RegExp(`^${stem}\\.7z(\\.\\d+)?$`, "i");
} }
} }
if (!pattern && ARCHIVE_GENERIC_001_RE.test(entryLower) && !ARCHIVE_KNOWN_001_RE.test(entryLower)) { if (!pattern && /^(.*)\.001$/.test(entryLower) && !/\.(zip|7z)\.001$/.test(entryLower)) {
const genericSplitMatch = entryLower.match(ARCHIVE_GENERIC_001_RE); const genericSplitMatch = entryLower.match(/^(.*)\.001$/);
if (genericSplitMatch) { if (genericSplitMatch) {
const stem = genericSplitMatch[1].replace(REGEX_ESCAPE_RE, "\\$&"); const stem = genericSplitMatch[1].replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
pattern = new RegExp(`^${stem}\\.\\d{3}$`, "i"); pattern = new RegExp(`^${stem}\\.\\d{3}$`, "i");
} }
} }
@ -7339,22 +7328,6 @@ export class DownloadManager extends EventEmitter {
logger.info(`Soft-Reset: Provider-Failures zurückgesetzt für ${provider}`); logger.info(`Soft-Reset: Provider-Failures zurückgesetzt für ${provider}`);
} }
} }
// Prune AllDebrid host info cache entries older than 5 min (TTL is 60s,
// so 5 min is well past usable - just unbounded growth otherwise).
let allDebridPruned = 0;
for (const [host, entry] of this.allDebridHostInfoCache) {
if (now - entry.cachedAt > 5 * 60 * 1000) {
this.allDebridHostInfoCache.delete(host);
allDebridPruned += 1;
}
}
// Prune expired Debrid-Link / Mega-Debrid runtime state (module-level Maps
// that would otherwise grow over 24/7 operation).
const dlPruned = pruneExpiredDebridLinkRuntimeState(now);
const mdPruned = pruneExpiredMegaDebridRuntimeState(now);
if (allDebridPruned > 0 || dlPruned > 0 || mdPruned > 0) {
logger.info(`Soft-Reset: pruned ${allDebridPruned} AllDebrid host entries, ${dlPruned} Debrid-Link entries, ${mdPruned} Mega-Debrid entries`);
}
} }
// ── Scheduler ────────────────────────────────────────────────────────── // ── Scheduler ──────────────────────────────────────────────────────────

View File

@ -1325,20 +1325,11 @@ const BandwidthChart = memo(function BandwidthChart({ items, running, paused, sp
}, [running, paused]); }, [running, paused]);
useEffect(() => { useEffect(() => {
// Always draw once on mount / when running/paused state changes so the
// chart shows the latest history.
drawChart();
// Only schedule periodic redraws while actively downloading — when
// stopped or paused the speed history doesn't change, so polling
// every 250ms would just burn CPU on the renderer process.
if (!running || paused) {
return;
}
const interval = setInterval(() => { const interval = setInterval(() => {
drawChart(); drawChart();
}, 250); }, 250);
return () => clearInterval(interval); return () => clearInterval(interval);
}, [drawChart, running, paused]); }, [drawChart]);
useEffect(() => { useEffect(() => {
// Only record samples while the session is running and not paused // Only record samples while the session is running and not paused