♻️ refactor(bestdebrid): switch from browser login to cookie file import
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 <noreply@anthropic.com>
This commit is contained in:
parent
d776ad589d
commit
153318274d
@ -201,8 +201,8 @@ export class AppController {
|
||||
await this.allDebridWebFallback.openLoginWindow();
|
||||
}
|
||||
|
||||
public async openBestDebridLoginWindow(): Promise<void> {
|
||||
await this.bestDebridWebFallback.openLoginWindow();
|
||||
public async importBestDebridCookies(filePath: string): Promise<number> {
|
||||
return this.bestDebridWebFallback.importCookiesFromFile(filePath);
|
||||
}
|
||||
|
||||
public async getAllDebridHostInfo(host = "rapidgator"): Promise<AllDebridHostInfo> {
|
||||
|
||||
@ -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<void> {
|
||||
if (!signal) {
|
||||
await sleep(ms);
|
||||
return;
|
||||
}
|
||||
if (signal.aborted) {
|
||||
throw abortError();
|
||||
}
|
||||
|
||||
await new Promise<void>((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<string, unknown> | null {
|
||||
try {
|
||||
const parsed = JSON.parse(text) as unknown;
|
||||
@ -72,12 +41,44 @@ function parseJson(text: string): Record<string, unknown> | 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<unknown> = 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<UnrestrictedLink | null> {
|
||||
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<void> {
|
||||
const window = await this.ensureLoginWindow();
|
||||
if (window.isMinimized()) {
|
||||
window.restore();
|
||||
public async importCookiesFromFile(filePath: string): Promise<number> {
|
||||
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<void> {
|
||||
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<T>(job: () => Promise<T>, signal?: AbortSignal): Promise<T> {
|
||||
const queuedAt = Date.now();
|
||||
const queueWaitTimeoutMs = 90_000;
|
||||
@ -162,72 +182,7 @@ export class BestDebridWebFallback {
|
||||
return run;
|
||||
}
|
||||
|
||||
private async ensureLoginWindow(): Promise<BrowserWindow> {
|
||||
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<GenerateOutcome> {
|
||||
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("<!") || text.trim().startsWith("<html")) {
|
||||
return { kind: "login_required" };
|
||||
}
|
||||
@ -259,9 +213,7 @@ export class BestDebridWebFallback {
|
||||
const error = Number(payload.error ?? -1);
|
||||
const message = String(payload.message || "").trim();
|
||||
|
||||
// error != 0 means failure
|
||||
if (error !== 0) {
|
||||
// Check if it's a login/auth issue
|
||||
if (/login|log in|sign in|not logged|session|auth/i.test(message)) {
|
||||
return { kind: "login_required" };
|
||||
}
|
||||
@ -277,11 +229,10 @@ export class BestDebridWebFallback {
|
||||
const fileSizeRaw = String(payload.size || "").trim();
|
||||
let fileSize: number | null = null;
|
||||
if (fileSizeRaw) {
|
||||
// Size might be like "96.63 MB" — parse it
|
||||
const match = fileSizeRaw.match(/([\d.]+)\s*(KB|MB|GB|TB|B)/i);
|
||||
const match = fileSizeRaw.match(/([\d.]+)\s*(KB|KiB|MB|MiB|GB|GiB|TB|TiB|B)/i);
|
||||
if (match) {
|
||||
const value = parseFloat(match[1]);
|
||||
const unit = match[2].toUpperCase();
|
||||
const unit = match[2].toUpperCase().replace("IB", "B");
|
||||
const multipliers: Record<string, number> = { 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<UnrestrictedLink | null> {
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -54,7 +54,7 @@ const api: ElectronApi = {
|
||||
openSessionLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_SESSION_LOG),
|
||||
openRealDebridLogin: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_REALDEBRID_LOGIN),
|
||||
openAllDebridLogin: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_ALLDEBRID_LOGIN),
|
||||
openBestDebridLogin: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_BESTDEBRID_LOGIN),
|
||||
importBestDebridCookies: (): Promise<number> => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_BESTDEBRID_COOKIES),
|
||||
getAllDebridHostInfo: (): Promise<AllDebridHostInfo> => ipcRenderer.invoke(IPC_CHANNELS.GET_ALLDEBRID_HOST_INFO),
|
||||
retryExtraction: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.RETRY_EXTRACTION, packageId),
|
||||
extractNow: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.EXTRACT_NOW, packageId),
|
||||
|
||||
@ -1156,13 +1156,17 @@ export function App(): ReactElement {
|
||||
});
|
||||
};
|
||||
|
||||
const onOpenBestDebridLogin = async (): Promise<void> => {
|
||||
const onImportBestDebridCookies = async (): Promise<void> => {
|
||||
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 {
|
||||
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.megaDebridPreferApi} onChange={(e) => setBool("megaDebridPreferApi", e.target.checked)} /> Mega-Debrid bevorzugt über API (schneller, Fallback auf Web)</label>
|
||||
<label>BestDebrid API Token</label>
|
||||
<input type="password" value={settingsDraft.bestToken} onChange={(e) => setText("bestToken", e.target.value)} />
|
||||
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.bestDebridUseWebLogin} onChange={(e) => setBool("bestDebridUseWebLogin", e.target.checked)} /> BestDebrid per Web-Login statt API-Token verwenden</label>
|
||||
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.bestDebridUseWebLogin} onChange={(e) => setBool("bestDebridUseWebLogin", e.target.checked)} /> BestDebrid per Cookie-Import statt API-Token verwenden</label>
|
||||
{settingsDraft.bestDebridUseWebLogin && (
|
||||
<>
|
||||
<div className="hint">Beim ersten Link oder über den Button unten öffnet sich ein BestDebrid-Browserfenster. Der Login läuft dort manuell über die Website.</div>
|
||||
<button className="btn" disabled={actionBusy} onClick={() => { void onOpenBestDebridLogin(); }}>BestDebrid Web-Login öffnen</button>
|
||||
<div className="hint">Exportiere deine BestDebrid-Cookies als Netscape-Textdatei (z.B. mit der Browser-Extension "Get cookies.txt LOCALLY") und importiere sie hier.</div>
|
||||
<button className="btn" disabled={actionBusy} onClick={() => { void onImportBestDebridCookies(); }}>BestDebrid Cookies importieren</button>
|
||||
</>
|
||||
)}
|
||||
<label>AllDebrid API Key</label>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -49,7 +49,7 @@ export interface ElectronApi {
|
||||
openSessionLog: () => Promise<void>;
|
||||
openRealDebridLogin: () => Promise<void>;
|
||||
openAllDebridLogin: () => Promise<void>;
|
||||
openBestDebridLogin: () => Promise<void>;
|
||||
importBestDebridCookies: () => Promise<number>;
|
||||
getAllDebridHostInfo: () => Promise<AllDebridHostInfo>;
|
||||
retryExtraction: (packageId: string) => Promise<void>;
|
||||
extractNow: (packageId: string) => Promise<void>;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user