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;