From 153318274dcf1cbd470fa0505451acad5124f4dd Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Fri, 6 Mar 2026 11:44:23 +0100 Subject: [PATCH] =?UTF-8?q?=E2=99=BB=EF=B8=8F=20refactor(bestdebrid):=20sw?= =?UTF-8?q?itch=20from=20browser=20login=20to=20cookie=20file=20import?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace BrowserWindow-based login flow with Netscape cookie file import for BestDebrid authentication. Cloudflare Turnstile captcha cannot be solved in Electron's embedded browser, so users export cookies from their real browser and import them here. - Rewrite bestdebrid-web.ts: remove BrowserWindow/CDP code, add parseNetscapeCookieFile() and importCookiesFromFile() - Add file picker dialog for .txt cookie files in main IPC handler - Update IPC channel from OPEN_BESTDEBRID_LOGIN to IMPORT_BESTDEBRID_COOKIES - Update preload bridge and renderer UI with cookie import button - Fix pLabel scope in downloadToFile (pass as parameter from processItem) Co-Authored-By: Claude Opus 4.6 --- src/main/app-controller.ts | 4 +- src/main/bestdebrid-web.ts | 246 ++++++++++++----------------------- src/main/download-manager.ts | 18 +-- src/main/main.ts | 15 ++- src/preload/preload.ts | 2 +- src/renderer/App.tsx | 18 ++- src/shared/ipc.ts | 2 +- src/shared/preload-api.ts | 2 +- 8 files changed, 123 insertions(+), 184 deletions(-) diff --git a/src/main/app-controller.ts b/src/main/app-controller.ts index ee57fec..2c742c2 100644 --- a/src/main/app-controller.ts +++ b/src/main/app-controller.ts @@ -201,8 +201,8 @@ export class AppController { await this.allDebridWebFallback.openLoginWindow(); } - public async openBestDebridLoginWindow(): Promise { - await this.bestDebridWebFallback.openLoginWindow(); + public async importBestDebridCookies(filePath: string): Promise { + return this.bestDebridWebFallback.importCookiesFromFile(filePath); } public async getAllDebridHostInfo(host = "rapidgator"): Promise { diff --git a/src/main/bestdebrid-web.ts b/src/main/bestdebrid-web.ts index 5480b6e..82adcaa 100644 --- a/src/main/bestdebrid-web.ts +++ b/src/main/bestdebrid-web.ts @@ -1,18 +1,16 @@ -import { BrowserWindow, session } from "electron"; +import fs from "node:fs"; +import { session } from "electron"; import { UnrestrictedLink } from "./realdebrid"; import { filenameFromUrl, sleep } from "./utils"; +import { logger } from "./logger"; const BESTDEBRID_BASE_URL = "https://bestdebrid.com"; -const BESTDEBRID_LOGIN_URL = `${BESTDEBRID_BASE_URL}/en/downloader/`; +const BESTDEBRID_DOWNLOADER_URL = `${BESTDEBRID_BASE_URL}/en/downloader/`; const BESTDEBRID_GENERATE_URL = `${BESTDEBRID_BASE_URL}/api/v1/generateLink`; const BESTDEBRID_PERSISTENT_PARTITION = "persist:bestdebrid-web"; const BESTDEBRID_TRANSIENT_PARTITION = "bestdebrid-web"; const BESTDEBRID_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"; -type GenerateOutcome = - | { kind: "success"; value: UnrestrictedLink } - | { kind: "login_required" }; - function abortError(): Error { return new Error("aborted:bestdebrid-web"); } @@ -31,35 +29,6 @@ function throwIfAborted(signal?: AbortSignal): void { } } -async function sleepWithSignal(ms: number, signal?: AbortSignal): Promise { - if (!signal) { - await sleep(ms); - return; - } - if (signal.aborted) { - throw abortError(); - } - - await new Promise((resolve, reject) => { - let timer: NodeJS.Timeout | null = setTimeout(() => { - timer = null; - signal.removeEventListener("abort", onAbort); - resolve(); - }, Math.max(0, ms)); - - const onAbort = (): void => { - if (timer) { - clearTimeout(timer); - timer = null; - } - signal.removeEventListener("abort", onAbort); - reject(abortError()); - }; - - signal.addEventListener("abort", onAbort, { once: true }); - }); -} - function parseJson(text: string): Record | null { try { const parsed = JSON.parse(text) as unknown; @@ -72,12 +41,44 @@ function parseJson(text: string): Record | null { } } +interface NetscapeCookie { + domain: string; + httpOnly: boolean; + path: string; + secure: boolean; + expirationDate: number; + name: string; + value: string; +} + +function parseNetscapeCookieFile(text: string): NetscapeCookie[] { + const cookies: NetscapeCookie[] = []; + for (const line of text.split(/\r?\n/)) { + const trimmed = line.trim(); + if (!trimmed || trimmed.startsWith("#")) { + continue; + } + const parts = trimmed.split("\t"); + if (parts.length < 7) { + continue; + } + cookies.push({ + domain: parts[0], + httpOnly: parts[1].toUpperCase() === "TRUE", + path: parts[2], + secure: parts[3].toUpperCase() === "TRUE", + expirationDate: Number(parts[4]) || 0, + name: parts[5], + value: parts[6] + }); + } + return cookies; +} + export class BestDebridWebFallback { private queue: Promise = Promise.resolve(); - private loginWindow: BrowserWindow | null = null; - - private loginWindowPartition = ""; + private cookiesImported = false; private getRememberSession: () => boolean; @@ -86,32 +87,60 @@ export class BestDebridWebFallback { } public async unrestrict(link: string, signal?: AbortSignal): Promise { - const overallSignal = withTimeoutSignal(signal, 10 * 60 * 1000); + const overallSignal = withTimeoutSignal(signal, 60_000); return this.runExclusive(async () => { throwIfAborted(overallSignal); if (!String(link || "").trim()) { return null; } - const initial = await this.generate(link, overallSignal); - if (initial.kind === "success") { - return initial.value; + if (!this.cookiesImported) { + throw new Error("BestDebrid: Keine Cookies importiert. Bitte zuerst über Einstellungen eine Cookie-Datei importieren."); } - return this.waitForLoginAndGenerate(link, overallSignal); + + const result = await this.generate(link, overallSignal); + if (result.kind === "success") { + return result.value; + } + throw new Error("BestDebrid: Nicht eingeloggt. Bitte neue Cookie-Datei importieren."); }, overallSignal); } - public async openLoginWindow(): Promise { - const window = await this.ensureLoginWindow(); - if (window.isMinimized()) { - window.restore(); + public async importCookiesFromFile(filePath: string): Promise { + const text = fs.readFileSync(filePath, "utf-8"); + const cookies = parseNetscapeCookieFile(text); + const bestDebridCookies = cookies.filter((c) => + c.domain.includes("bestdebrid.com") + ); + + if (bestDebridCookies.length === 0) { + throw new Error("Keine BestDebrid-Cookies in der Datei gefunden"); } - window.show(); - window.focus(); + + const currentSession = session.fromPartition(this.getPartition()); + currentSession.setUserAgent(BESTDEBRID_USER_AGENT); + + for (const cookie of bestDebridCookies) { + const url = `https://${cookie.domain.replace(/^\./, "")}${cookie.path}`; + await currentSession.cookies.set({ + url, + name: cookie.name, + value: cookie.value, + domain: cookie.domain, + path: cookie.path, + secure: cookie.secure, + httpOnly: cookie.httpOnly, + expirationDate: cookie.expirationDate > 0 ? cookie.expirationDate : undefined + }); + } + + this.cookiesImported = true; + logger.info(`BestDebrid: ${bestDebridCookies.length} Cookies importiert aus ${filePath}`); + return bestDebridCookies.length; } public async clearSessions(): Promise { - this.disposeLoginWindow(); + this.cookiesImported = false; for (const partition of [BESTDEBRID_PERSISTENT_PARTITION, BESTDEBRID_TRANSIENT_PARTITION]) { const currentSession = session.fromPartition(partition); try { @@ -130,22 +159,13 @@ export class BestDebridWebFallback { } public dispose(): void { - this.disposeLoginWindow(); + // nothing to clean up } private getPartition(): string { return this.getRememberSession() ? BESTDEBRID_PERSISTENT_PARTITION : BESTDEBRID_TRANSIENT_PARTITION; } - private disposeLoginWindow(): void { - const current = this.loginWindow; - this.loginWindow = null; - this.loginWindowPartition = ""; - if (current && !current.isDestroyed()) { - current.close(); - } - } - private async runExclusive(job: () => Promise, signal?: AbortSignal): Promise { const queuedAt = Date.now(); const queueWaitTimeoutMs = 90_000; @@ -162,72 +182,7 @@ export class BestDebridWebFallback { return run; } - private async ensureLoginWindow(): Promise { - const partition = this.getPartition(); - const existing = this.loginWindow; - if (existing && !existing.isDestroyed() && this.loginWindowPartition === partition) { - return existing; - } - - if (existing && !existing.isDestroyed()) { - existing.close(); - } - - // Set user agent on session level so Cloudflare Turnstile sees a real Chrome - const currentSession = session.fromPartition(partition); - currentSession.setUserAgent(BESTDEBRID_USER_AGENT); - - const window = new BrowserWindow({ - width: 1120, - height: 900, - minWidth: 980, - minHeight: 760, - autoHideMenuBar: true, - title: "BestDebrid Web-Login", - webPreferences: { - partition, - contextIsolation: true, - nodeIntegration: false - } - }); - window.webContents.setUserAgent(BESTDEBRID_USER_AGENT); - window.setMenuBarVisibility(false); - - // Inject anti-fingerprint patches via CDP before any page JS runs. - // The debugger must stay attached until after page load so the - // registered scripts actually execute on every new document. - let debuggerAttached = false; - try { - window.webContents.debugger.attach("1.3"); - debuggerAttached = true; - await window.webContents.debugger.sendCommand("Page.addScriptToEvaluateOnNewDocument", { - source: [ - "Object.defineProperty(navigator, 'webdriver', { get: () => false });", - "Object.defineProperty(navigator, 'plugins', { get: () => [1, 2, 3, 4, 5] });", - "Object.defineProperty(navigator, 'languages', { get: () => ['de-DE', 'de', 'en-US', 'en'] });", - "window.chrome = { runtime: {}, loadTimes: function() {}, csi: function() {} };" - ].join("\n") - }); - } catch { - // CDP not available — continue without patches - } - - window.on("closed", () => { - if (debuggerAttached) { - try { window.webContents.debugger.detach(); } catch { /* already detached */ } - } - if (this.loginWindow === window) { - this.loginWindow = null; - this.loginWindowPartition = ""; - } - }); - this.loginWindow = window; - this.loginWindowPartition = partition; - await window.loadURL(BESTDEBRID_LOGIN_URL); - return window; - } - - private async generate(link: string, signal?: AbortSignal): Promise { + private async generate(link: string, signal?: AbortSignal): Promise<{ kind: "success"; value: UnrestrictedLink } | { kind: "login_required" }> { throwIfAborted(signal); const currentSession = session.fromPartition(this.getPartition()); const response = await currentSession.fetch(BESTDEBRID_GENERATE_URL, { @@ -236,7 +191,7 @@ export class BestDebridWebFallback { Accept: "application/json, text/javascript, */*; q=0.01", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", Origin: BESTDEBRID_BASE_URL, - Referer: BESTDEBRID_LOGIN_URL, + Referer: BESTDEBRID_DOWNLOADER_URL, "User-Agent": BESTDEBRID_USER_AGENT, "X-Requested-With": "XMLHttpRequest" }, @@ -246,7 +201,6 @@ export class BestDebridWebFallback { const text = await response.text(); - // Not logged in — BestDebrid redirects or returns HTML login page if (!response.ok || text.trim().startsWith(" = { B: 1, KB: 1024, MB: 1024 * 1024, GB: 1024 * 1024 * 1024, TB: 1024 * 1024 * 1024 * 1024 }; fileSize = Math.floor(value * (multipliers[unit] || 1)); } @@ -297,33 +248,4 @@ export class BestDebridWebFallback { } }; } - - private async waitForLoginAndGenerate(link: string, signal?: AbortSignal): Promise { - const window = await this.ensureLoginWindow(); - if (window.isMinimized()) { - window.restore(); - } - window.show(); - window.focus(); - - const startedAt = Date.now(); - while (Date.now() - startedAt < 10 * 60 * 1000) { - throwIfAborted(signal); - if (window.isDestroyed()) { - throw new Error("BestDebrid Web-Login abgebrochen"); - } - - const outcome = await this.generate(link, signal); - if (outcome.kind === "success") { - if (!window.isDestroyed()) { - window.close(); - } - return outcome.value; - } - - await sleepWithSignal(1_500, signal); - } - - throw new Error("BestDebrid Web-Login Timeout"); - } } diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index ecddc75..4d951c5 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -4789,7 +4789,7 @@ export class DownloadManager extends EventEmitter { item.updatedAt = nowMs(); this.emitState(); } - const result = await this.downloadToFile(active, unrestricted.directUrl, item.targetPath, item.totalBytes, unrestricted.skipTlsVerify); + const result = await this.downloadToFile(active, unrestricted.directUrl, item.targetPath, item.totalBytes, unrestricted.skipTlsVerify, pLabel); active.resumable = result.resumable; if (!active.resumable && !active.nonResumableCounted) { active.nonResumableCounted = true; @@ -5176,8 +5176,10 @@ export class DownloadManager extends EventEmitter { directUrl: string, targetPath: string, knownTotal: number | null, - skipTlsVerify?: boolean + skipTlsVerify?: boolean, + pLabel?: string ): Promise<{ resumable: boolean }> { + const label = pLabel || providerLabel(this.session.items[active.itemId]?.provider); const item = this.session.items[active.itemId]; if (!item) { throw new Error("Download-Item fehlt"); @@ -5426,7 +5428,7 @@ export class DownloadManager extends EventEmitter { if (nowTick - lastDiskBusyEmitAt >= 1200) { item.status = "downloading"; item.speedBps = 0; - item.fullStatus = `Warte auf Festplatte (${pLabel})`; + item.fullStatus = `Warte auf Festplatte (${label})`; item.updatedAt = nowTick; this.emitState(); lastDiskBusyEmitAt = nowTick; @@ -5537,7 +5539,7 @@ export class DownloadManager extends EventEmitter { if (nowTick - lastIdleEmitAt >= idlePulseMs) { item.status = "downloading"; item.speedBps = 0; - item.fullStatus = `Warte auf Festplatte (${pLabel})`; + item.fullStatus = `Warte auf Festplatte (${label})`; item.updatedAt = nowTick; this.emitState(); lastIdleEmitAt = nowTick; @@ -5553,7 +5555,7 @@ export class DownloadManager extends EventEmitter { } item.status = "downloading"; item.speedBps = 0; - item.fullStatus = `Warte auf Daten (${pLabel})`; + item.fullStatus = `Warte auf Daten (${label})`; if (nowTick - lastIdleEmitAt >= idlePulseMs) { item.updatedAt = nowTick; this.emitState(); @@ -5662,7 +5664,7 @@ export class DownloadManager extends EventEmitter { if (nowTick - lastDiskBusyEmitAt >= 1200) { item.status = "downloading"; item.speedBps = 0; - item.fullStatus = `Warte auf Festplatte (${pLabel})`; + item.fullStatus = `Warte auf Festplatte (${label})`; item.updatedAt = nowTick; this.emitState(); lastDiskBusyEmitAt = nowTick; @@ -5706,10 +5708,10 @@ export class DownloadManager extends EventEmitter { const diskBusy = diskBusySince > 0 && nowMs() - diskBusySince >= DISK_BUSY_THRESHOLD_MS; if (diskBusy) { item.speedBps = 0; - item.fullStatus = `Warte auf Festplatte (${pLabel})`; + item.fullStatus = `Warte auf Festplatte (${label})`; } else { item.speedBps = Math.max(0, Math.floor(speed)); - item.fullStatus = `Download läuft (${pLabel})`; + item.fullStatus = `Download läuft (${label})`; } const nowTick = nowMs(); if (nowTick - lastUiEmitAt >= uiUpdateIntervalMs) { diff --git a/src/main/main.ts b/src/main/main.ts index 694f9e0..737f95d 100644 --- a/src/main/main.ts +++ b/src/main/main.ts @@ -454,8 +454,19 @@ function registerIpcHandlers(): void { await controller.openAllDebridLoginWindow(); }); - ipcMain.handle(IPC_CHANNELS.OPEN_BESTDEBRID_LOGIN, async () => { - await controller.openBestDebridLoginWindow(); + ipcMain.handle(IPC_CHANNELS.IMPORT_BESTDEBRID_COOKIES, async () => { + const options = { + properties: ["openFile"] as Array<"openFile">, + filters: [ + { name: "Cookie-Datei", extensions: ["txt"] }, + { name: "Alle Dateien", extensions: ["*"] } + ] + }; + const result = mainWindow ? await dialog.showOpenDialog(mainWindow, options) : await dialog.showOpenDialog(options); + if (result.canceled || result.filePaths.length === 0) { + return 0; + } + return controller.importBestDebridCookies(result.filePaths[0]); }); ipcMain.handle(IPC_CHANNELS.GET_ALLDEBRID_HOST_INFO, async () => { diff --git a/src/preload/preload.ts b/src/preload/preload.ts index 6a40051..841cd3e 100644 --- a/src/preload/preload.ts +++ b/src/preload/preload.ts @@ -54,7 +54,7 @@ const api: ElectronApi = { openSessionLog: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.OPEN_SESSION_LOG), openRealDebridLogin: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.OPEN_REALDEBRID_LOGIN), openAllDebridLogin: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.OPEN_ALLDEBRID_LOGIN), - openBestDebridLogin: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.OPEN_BESTDEBRID_LOGIN), + importBestDebridCookies: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_BESTDEBRID_COOKIES), getAllDebridHostInfo: (): Promise => ipcRenderer.invoke(IPC_CHANNELS.GET_ALLDEBRID_HOST_INFO), retryExtraction: (packageId: string): Promise => ipcRenderer.invoke(IPC_CHANNELS.RETRY_EXTRACTION, packageId), extractNow: (packageId: string): Promise => ipcRenderer.invoke(IPC_CHANNELS.EXTRACT_NOW, packageId), diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index db6acb8..e81ffdf 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -1156,13 +1156,17 @@ export function App(): ReactElement { }); }; - const onOpenBestDebridLogin = async (): Promise => { + const onImportBestDebridCookies = async (): Promise => { await performQuickAction(async () => { await persistDraftSettings(); - await window.rd.openBestDebridLogin(); - showToast("BestDebrid Login-Fenster geöffnet", 2200); + const count = await window.rd.importBestDebridCookies(); + if (count > 0) { + showToast(`${count} BestDebrid-Cookies importiert`, 2200); + } else { + showToast("Keine Cookie-Datei ausgewählt", 2200); + } }, (error) => { - showToast(`BestDebrid Login fehlgeschlagen: ${String(error)}`, 2800); + showToast(`BestDebrid Cookie-Import fehlgeschlagen: ${String(error)}`, 2800); }); }; @@ -2854,11 +2858,11 @@ export function App(): ReactElement { setText("bestToken", e.target.value)} /> - + {settingsDraft.bestDebridUseWebLogin && ( <> -
Beim ersten Link oder über den Button unten öffnet sich ein BestDebrid-Browserfenster. Der Login läuft dort manuell über die Website.
- +
Exportiere deine BestDebrid-Cookies als Netscape-Textdatei (z.B. mit der Browser-Extension "Get cookies.txt LOCALLY") und importiere sie hier.
+ )} diff --git a/src/shared/ipc.ts b/src/shared/ipc.ts index 0efc8eb..d39ed65 100644 --- a/src/shared/ipc.ts +++ b/src/shared/ipc.ts @@ -36,7 +36,7 @@ export const IPC_CHANNELS = { OPEN_SESSION_LOG: "app:open-session-log", OPEN_REALDEBRID_LOGIN: "app:open-realdebrid-login", OPEN_ALLDEBRID_LOGIN: "app:open-alldebrid-login", - OPEN_BESTDEBRID_LOGIN: "app:open-bestdebrid-login", + IMPORT_BESTDEBRID_COOKIES: "app:import-bestdebrid-cookies", GET_ALLDEBRID_HOST_INFO: "app:get-alldebrid-host-info", RETRY_EXTRACTION: "queue:retry-extraction", EXTRACT_NOW: "queue:extract-now", diff --git a/src/shared/preload-api.ts b/src/shared/preload-api.ts index fc52af7..1703bb0 100644 --- a/src/shared/preload-api.ts +++ b/src/shared/preload-api.ts @@ -49,7 +49,7 @@ export interface ElectronApi { openSessionLog: () => Promise; openRealDebridLogin: () => Promise; openAllDebridLogin: () => Promise; - openBestDebridLogin: () => Promise; + importBestDebridCookies: () => Promise; getAllDebridHostInfo: () => Promise; retryExtraction: (packageId: string) => Promise; extractNow: (packageId: string) => Promise;