diff --git a/package.json b/package.json index bbe7aa8..750922d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "real-debrid-downloader", - "version": "1.5.84", + "version": "1.5.85", "description": "Real-Debrid Downloader Desktop (Electron + React + TypeScript)", "main": "build/main/main/main.js", "author": "Sucukdeluxe", diff --git a/src/main/debrid.ts b/src/main/debrid.ts index 3bfc490..3890fc2 100644 --- a/src/main/debrid.ts +++ b/src/main/debrid.ts @@ -485,7 +485,57 @@ export async function checkRapidgatorOnline( return null; } const fileId = fileIdMatch[1]; + const headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", + Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", + "Accept-Language": "en-US,en;q=0.9,de;q=0.8" + }; + // Fast path: HEAD request (no body download, much faster) + for (let attempt = 1; attempt <= REQUEST_RETRIES + 1; attempt += 1) { + try { + if (signal?.aborted) throw new Error("aborted:debrid"); + + const response = await fetch(link, { + method: "HEAD", + redirect: "follow", + headers, + signal: withTimeoutSignal(signal, 15000) + }); + + if (response.status === 404) { + return { online: false, fileName: "", fileSize: null }; + } + + if (response.ok) { + const finalUrl = response.url || link; + if (!finalUrl.includes(fileId)) { + return { online: false, fileName: "", fileSize: null }; + } + // HEAD 200 + URL still contains file ID → online + const fileName = filenameFromRapidgatorUrlPath(link); + return { online: true, fileName, fileSize: null }; + } + + // Non-OK, non-404: retry or give up + if (shouldRetryStatus(response.status) && attempt <= REQUEST_RETRIES) { + await sleepWithSignal(retryDelayForResponse(response, attempt), signal); + continue; + } + + // HEAD inconclusive — fall through to GET + break; + } catch (error) { + const errorText = compactErrorText(error); + if (/aborted/i.test(errorText)) throw error; + if (attempt > REQUEST_RETRIES || !isRetryableErrorText(errorText)) { + break; // fall through to GET + } + await sleepWithSignal(retryDelay(attempt), signal); + } + } + + // Slow path: GET request (downloads HTML, more thorough) for (let attempt = 1; attempt <= REQUEST_RETRIES + 1; attempt += 1) { try { if (signal?.aborted) throw new Error("aborted:debrid"); @@ -493,11 +543,7 @@ export async function checkRapidgatorOnline( const response = await fetch(link, { method: "GET", redirect: "follow", - headers: { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", - Accept: "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", - "Accept-Language": "en-US,en;q=0.9,de;q=0.8" - }, + headers, signal: withTimeoutSignal(signal, API_TIMEOUT_MS) }); diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 58ac44c..8af1baa 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -838,6 +838,7 @@ export class DownloadManager extends EventEmitter { void this.recoverRetryableItems("startup").catch((err) => logger.warn(`recoverRetryableItems Fehler (startup): ${compactErrorText(err)}`)); this.recoverPostProcessingOnStartup(); this.resolveExistingQueuedOpaqueFilenames(); + this.checkExistingRapidgatorLinks(); void this.cleanupExistingExtractedArchives().catch((err) => logger.warn(`cleanupExistingExtractedArchives Fehler (constructor): ${compactErrorText(err)}`)); } @@ -1534,49 +1535,28 @@ export class DownloadManager extends EventEmitter { this.emitState(); } - const uniqueUrls = [...new Set(itemsToCheck.map(i => i.url))]; - const concurrency = 4; - const queue = [...uniqueUrls]; - const workers = Array.from({ length: Math.min(concurrency, queue.length) }, async () => { - while (queue.length > 0) { - const url = queue.shift()!; - const result = await checkRapidgatorOnline(url); - if (result !== null) checked.set(url, result); - } - }); - await Promise.all(workers); + // Check links one by one (sequentially) so the user sees dots change progressively + const checkedUrls = new Map>>(); - if (checked.size === 0) return; - - let changed = false; for (const { itemId, url } of itemsToCheck) { - const result = checked.get(url); - if (!result) continue; const item = this.session.items[itemId]; - if (!item || item.status !== "queued") continue; + if (!item) continue; - if (!result.online) { - item.status = "failed"; - item.fullStatus = "Offline"; - item.lastError = "Datei nicht gefunden auf Rapidgator"; - item.onlineStatus = "offline"; - item.updatedAt = nowMs(); - changed = true; - } else { - if (result.fileName && looksLikeOpaqueFilename(item.fileName)) { - item.fileName = sanitizeFilename(result.fileName); - this.assignItemTargetPath(item, path.join(this.session.packages[item.packageId]?.outputDir || this.settings.outputDir, item.fileName)); - } - item.onlineStatus = "online"; - item.updatedAt = nowMs(); - changed = true; + // Reuse result if same URL was already checked + if (checkedUrls.has(url)) { + const cached = checkedUrls.get(url); + this.applyRapidgatorCheckResult(item, cached); + this.emitState(); + continue; } - } - if (changed) { - this.persistSoon(); + const result = await checkRapidgatorOnline(url); + checkedUrls.set(url, result); + this.applyRapidgatorCheckResult(item, result); this.emitState(); } + + this.persistSoon(); } private resolveExistingQueuedOpaqueFilenames(): void { @@ -1602,6 +1582,43 @@ export class DownloadManager extends EventEmitter { } } + private applyRapidgatorCheckResult(item: DownloadItem, result: Awaited>): void { + if (!result) { + if (item.onlineStatus === "checking") { + item.onlineStatus = undefined; + } + return; + } + if (item.status !== "queued") return; + + if (!result.online) { + item.status = "failed"; + item.fullStatus = "Offline"; + item.lastError = "Datei nicht gefunden auf Rapidgator"; + item.onlineStatus = "offline"; + item.updatedAt = nowMs(); + } else { + if (result.fileName && looksLikeOpaqueFilename(item.fileName)) { + item.fileName = sanitizeFilename(result.fileName); + this.assignItemTargetPath(item, path.join(this.session.packages[item.packageId]?.outputDir || this.settings.outputDir, item.fileName)); + } + item.onlineStatus = "online"; + item.updatedAt = nowMs(); + } + } + + private checkExistingRapidgatorLinks(): void { + const uncheckedIds: string[] = []; + for (const item of Object.values(this.session.items)) { + if (item.status !== "queued") continue; + if (item.onlineStatus) continue; // already checked + uncheckedIds.push(item.id); + } + if (uncheckedIds.length > 0) { + void this.checkRapidgatorLinks(uncheckedIds).catch((err) => logger.warn(`checkRapidgatorLinks Fehler (startup): ${compactErrorText(err)}`)); + } + } + private async cleanupExistingExtractedArchives(): Promise { if (this.settings.cleanupMode === "none") { return; @@ -2803,10 +2820,14 @@ export class DownloadManager extends EventEmitter { // Clear stale transient status texts from previous session if (item.status === "queued") { const fs = (item.fullStatus || "").trim(); - if (fs !== "Wartet" && fs !== "Paket gestoppt") { + if (fs !== "Wartet" && fs !== "Paket gestoppt" && fs !== "Online") { item.fullStatus = "Wartet"; } } + // Reset stale "checking" status from interrupted checks + if (item.onlineStatus === "checking") { + item.onlineStatus = undefined; + } if (item.status === "completed") { const fs = (item.fullStatus || "").trim(); if (fs && !isExtractedLabel(fs) && !/^Fertig\b/i.test(fs)) { diff --git a/src/renderer/styles.css b/src/renderer/styles.css index ad72eb8..1fcdf40 100644 --- a/src/renderer/styles.css +++ b/src/renderer/styles.css @@ -1335,11 +1335,6 @@ td { .link-status-dot.checking { background: #f59e0b; box-shadow: 0 0 4px #f59e0b80; - animation: pulse-dot 1s ease-in-out infinite; -} -@keyframes pulse-dot { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.4; } } .item-remove {