Compare commits

..

No commits in common. "67854feeb4ac857883c0d2f6e1553fa68e0ff023" and "d776ad589d5982572ad01462a831c6a1ad7302cb" have entirely different histories.

9 changed files with 185 additions and 124 deletions

View File

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

View File

@ -201,8 +201,8 @@ export class AppController {
await this.allDebridWebFallback.openLoginWindow(); await this.allDebridWebFallback.openLoginWindow();
} }
public async importBestDebridCookies(filePath: string): Promise<number> { public async openBestDebridLoginWindow(): Promise<void> {
return this.bestDebridWebFallback.importCookiesFromFile(filePath); await this.bestDebridWebFallback.openLoginWindow();
} }
public async getAllDebridHostInfo(host = "rapidgator"): Promise<AllDebridHostInfo> { public async getAllDebridHostInfo(host = "rapidgator"): Promise<AllDebridHostInfo> {

View File

@ -1,16 +1,18 @@
import fs from "node:fs"; import { BrowserWindow, session } from "electron";
import { session } from "electron";
import { UnrestrictedLink } from "./realdebrid"; import { UnrestrictedLink } from "./realdebrid";
import { filenameFromUrl, sleep } from "./utils"; import { filenameFromUrl, sleep } from "./utils";
import { logger } from "./logger";
const BESTDEBRID_BASE_URL = "https://bestdebrid.com"; const BESTDEBRID_BASE_URL = "https://bestdebrid.com";
const BESTDEBRID_DOWNLOADER_URL = `${BESTDEBRID_BASE_URL}/en/downloader/`; const BESTDEBRID_LOGIN_URL = `${BESTDEBRID_BASE_URL}/en/downloader/`;
const BESTDEBRID_GENERATE_URL = `${BESTDEBRID_BASE_URL}/api/v1/generateLink`; const BESTDEBRID_GENERATE_URL = `${BESTDEBRID_BASE_URL}/api/v1/generateLink`;
const BESTDEBRID_PERSISTENT_PARTITION = "persist:bestdebrid-web"; const BESTDEBRID_PERSISTENT_PARTITION = "persist:bestdebrid-web";
const BESTDEBRID_TRANSIENT_PARTITION = "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"; 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 { function abortError(): Error {
return new Error("aborted:bestdebrid-web"); return new Error("aborted:bestdebrid-web");
} }
@ -29,6 +31,35 @@ 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 { function parseJson(text: string): Record<string, unknown> | null {
try { try {
const parsed = JSON.parse(text) as unknown; const parsed = JSON.parse(text) as unknown;
@ -41,44 +72,12 @@ 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 { export class BestDebridWebFallback {
private queue: Promise<unknown> = Promise.resolve(); private queue: Promise<unknown> = Promise.resolve();
private cookiesImported = false; private loginWindow: BrowserWindow | null = null;
private loginWindowPartition = "";
private getRememberSession: () => boolean; private getRememberSession: () => boolean;
@ -87,60 +86,32 @@ export class BestDebridWebFallback {
} }
public async unrestrict(link: string, signal?: AbortSignal): Promise<UnrestrictedLink | null> { public async unrestrict(link: string, signal?: AbortSignal): Promise<UnrestrictedLink | null> {
const overallSignal = withTimeoutSignal(signal, 60_000); const overallSignal = withTimeoutSignal(signal, 10 * 60 * 1000);
return this.runExclusive(async () => { return this.runExclusive(async () => {
throwIfAborted(overallSignal); throwIfAborted(overallSignal);
if (!String(link || "").trim()) { if (!String(link || "").trim()) {
return null; return null;
} }
if (!this.cookiesImported) { const initial = await this.generate(link, overallSignal);
throw new Error("BestDebrid: Keine Cookies importiert. Bitte zuerst über Einstellungen eine Cookie-Datei importieren."); if (initial.kind === "success") {
return initial.value;
} }
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); }, overallSignal);
} }
public async importCookiesFromFile(filePath: string): Promise<number> { public async openLoginWindow(): Promise<void> {
const text = fs.readFileSync(filePath, "utf-8"); const window = await this.ensureLoginWindow();
const cookies = parseNetscapeCookieFile(text); if (window.isMinimized()) {
const bestDebridCookies = cookies.filter((c) => window.restore();
c.domain.includes("bestdebrid.com")
);
if (bestDebridCookies.length === 0) {
throw new Error("Keine BestDebrid-Cookies in der Datei gefunden");
} }
window.show();
const currentSession = session.fromPartition(this.getPartition()); window.focus();
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> { public async clearSessions(): Promise<void> {
this.cookiesImported = false; this.disposeLoginWindow();
for (const partition of [BESTDEBRID_PERSISTENT_PARTITION, BESTDEBRID_TRANSIENT_PARTITION]) { for (const partition of [BESTDEBRID_PERSISTENT_PARTITION, BESTDEBRID_TRANSIENT_PARTITION]) {
const currentSession = session.fromPartition(partition); const currentSession = session.fromPartition(partition);
try { try {
@ -159,13 +130,22 @@ export class BestDebridWebFallback {
} }
public dispose(): void { public dispose(): void {
// nothing to clean up this.disposeLoginWindow();
} }
private getPartition(): string { private getPartition(): string {
return this.getRememberSession() ? BESTDEBRID_PERSISTENT_PARTITION : BESTDEBRID_TRANSIENT_PARTITION; 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> { private async runExclusive<T>(job: () => Promise<T>, signal?: AbortSignal): Promise<T> {
const queuedAt = Date.now(); const queuedAt = Date.now();
const queueWaitTimeoutMs = 90_000; const queueWaitTimeoutMs = 90_000;
@ -182,7 +162,72 @@ export class BestDebridWebFallback {
return run; return run;
} }
private async generate(link: string, signal?: AbortSignal): Promise<{ kind: "success"; value: UnrestrictedLink } | { kind: "login_required" }> { 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> {
throwIfAborted(signal); throwIfAborted(signal);
const currentSession = session.fromPartition(this.getPartition()); const currentSession = session.fromPartition(this.getPartition());
const response = await currentSession.fetch(BESTDEBRID_GENERATE_URL, { const response = await currentSession.fetch(BESTDEBRID_GENERATE_URL, {
@ -191,7 +236,7 @@ export class BestDebridWebFallback {
Accept: "application/json, text/javascript, */*; q=0.01", Accept: "application/json, text/javascript, */*; q=0.01",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8", "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
Origin: BESTDEBRID_BASE_URL, Origin: BESTDEBRID_BASE_URL,
Referer: BESTDEBRID_DOWNLOADER_URL, Referer: BESTDEBRID_LOGIN_URL,
"User-Agent": BESTDEBRID_USER_AGENT, "User-Agent": BESTDEBRID_USER_AGENT,
"X-Requested-With": "XMLHttpRequest" "X-Requested-With": "XMLHttpRequest"
}, },
@ -201,6 +246,7 @@ export class BestDebridWebFallback {
const text = await response.text(); 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")) { if (!response.ok || text.trim().startsWith("<!") || text.trim().startsWith("<html")) {
return { kind: "login_required" }; return { kind: "login_required" };
} }
@ -213,7 +259,9 @@ export class BestDebridWebFallback {
const error = Number(payload.error ?? -1); const error = Number(payload.error ?? -1);
const message = String(payload.message || "").trim(); const message = String(payload.message || "").trim();
// error != 0 means failure
if (error !== 0) { if (error !== 0) {
// Check if it's a login/auth issue
if (/login|log in|sign in|not logged|session|auth/i.test(message)) { if (/login|log in|sign in|not logged|session|auth/i.test(message)) {
return { kind: "login_required" }; return { kind: "login_required" };
} }
@ -229,10 +277,11 @@ export class BestDebridWebFallback {
const fileSizeRaw = String(payload.size || "").trim(); const fileSizeRaw = String(payload.size || "").trim();
let fileSize: number | null = null; let fileSize: number | null = null;
if (fileSizeRaw) { if (fileSizeRaw) {
const match = fileSizeRaw.match(/([\d.]+)\s*(KB|KiB|MB|MiB|GB|GiB|TB|TiB|B)/i); // Size might be like "96.63 MB" — parse it
const match = fileSizeRaw.match(/([\d.]+)\s*(KB|MB|GB|TB|B)/i);
if (match) { if (match) {
const value = parseFloat(match[1]); const value = parseFloat(match[1]);
const unit = match[2].toUpperCase().replace("IB", "B"); const unit = match[2].toUpperCase();
const multipliers: Record<string, number> = { B: 1, KB: 1024, MB: 1024 * 1024, GB: 1024 * 1024 * 1024, TB: 1024 * 1024 * 1024 * 1024 }; 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)); fileSize = Math.floor(value * (multipliers[unit] || 1));
} }
@ -248,4 +297,33 @@ 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");
}
} }

View File

@ -4789,7 +4789,7 @@ export class DownloadManager extends EventEmitter {
item.updatedAt = nowMs(); item.updatedAt = nowMs();
this.emitState(); this.emitState();
} }
const result = await this.downloadToFile(active, unrestricted.directUrl, item.targetPath, item.totalBytes, unrestricted.skipTlsVerify, pLabel); const result = await this.downloadToFile(active, unrestricted.directUrl, item.targetPath, item.totalBytes, unrestricted.skipTlsVerify);
active.resumable = result.resumable; active.resumable = result.resumable;
if (!active.resumable && !active.nonResumableCounted) { if (!active.resumable && !active.nonResumableCounted) {
active.nonResumableCounted = true; active.nonResumableCounted = true;
@ -5176,10 +5176,8 @@ export class DownloadManager extends EventEmitter {
directUrl: string, directUrl: string,
targetPath: string, targetPath: string,
knownTotal: number | null, knownTotal: number | null,
skipTlsVerify?: boolean, skipTlsVerify?: boolean
pLabel?: string
): Promise<{ resumable: boolean }> { ): Promise<{ resumable: boolean }> {
const label = pLabel || providerLabel(this.session.items[active.itemId]?.provider);
const item = this.session.items[active.itemId]; const item = this.session.items[active.itemId];
if (!item) { if (!item) {
throw new Error("Download-Item fehlt"); throw new Error("Download-Item fehlt");
@ -5428,7 +5426,7 @@ export class DownloadManager extends EventEmitter {
if (nowTick - lastDiskBusyEmitAt >= 1200) { if (nowTick - lastDiskBusyEmitAt >= 1200) {
item.status = "downloading"; item.status = "downloading";
item.speedBps = 0; item.speedBps = 0;
item.fullStatus = `Warte auf Festplatte (${label})`; item.fullStatus = `Warte auf Festplatte (${pLabel})`;
item.updatedAt = nowTick; item.updatedAt = nowTick;
this.emitState(); this.emitState();
lastDiskBusyEmitAt = nowTick; lastDiskBusyEmitAt = nowTick;
@ -5539,7 +5537,7 @@ export class DownloadManager extends EventEmitter {
if (nowTick - lastIdleEmitAt >= idlePulseMs) { if (nowTick - lastIdleEmitAt >= idlePulseMs) {
item.status = "downloading"; item.status = "downloading";
item.speedBps = 0; item.speedBps = 0;
item.fullStatus = `Warte auf Festplatte (${label})`; item.fullStatus = `Warte auf Festplatte (${pLabel})`;
item.updatedAt = nowTick; item.updatedAt = nowTick;
this.emitState(); this.emitState();
lastIdleEmitAt = nowTick; lastIdleEmitAt = nowTick;
@ -5555,7 +5553,7 @@ export class DownloadManager extends EventEmitter {
} }
item.status = "downloading"; item.status = "downloading";
item.speedBps = 0; item.speedBps = 0;
item.fullStatus = `Warte auf Daten (${label})`; item.fullStatus = `Warte auf Daten (${pLabel})`;
if (nowTick - lastIdleEmitAt >= idlePulseMs) { if (nowTick - lastIdleEmitAt >= idlePulseMs) {
item.updatedAt = nowTick; item.updatedAt = nowTick;
this.emitState(); this.emitState();
@ -5664,7 +5662,7 @@ export class DownloadManager extends EventEmitter {
if (nowTick - lastDiskBusyEmitAt >= 1200) { if (nowTick - lastDiskBusyEmitAt >= 1200) {
item.status = "downloading"; item.status = "downloading";
item.speedBps = 0; item.speedBps = 0;
item.fullStatus = `Warte auf Festplatte (${label})`; item.fullStatus = `Warte auf Festplatte (${pLabel})`;
item.updatedAt = nowTick; item.updatedAt = nowTick;
this.emitState(); this.emitState();
lastDiskBusyEmitAt = nowTick; lastDiskBusyEmitAt = nowTick;
@ -5708,10 +5706,10 @@ export class DownloadManager extends EventEmitter {
const diskBusy = diskBusySince > 0 && nowMs() - diskBusySince >= DISK_BUSY_THRESHOLD_MS; const diskBusy = diskBusySince > 0 && nowMs() - diskBusySince >= DISK_BUSY_THRESHOLD_MS;
if (diskBusy) { if (diskBusy) {
item.speedBps = 0; item.speedBps = 0;
item.fullStatus = `Warte auf Festplatte (${label})`; item.fullStatus = `Warte auf Festplatte (${pLabel})`;
} else { } else {
item.speedBps = Math.max(0, Math.floor(speed)); item.speedBps = Math.max(0, Math.floor(speed));
item.fullStatus = `Download läuft (${label})`; item.fullStatus = `Download läuft (${pLabel})`;
} }
const nowTick = nowMs(); const nowTick = nowMs();
if (nowTick - lastUiEmitAt >= uiUpdateIntervalMs) { if (nowTick - lastUiEmitAt >= uiUpdateIntervalMs) {

View File

@ -454,19 +454,8 @@ function registerIpcHandlers(): void {
await controller.openAllDebridLoginWindow(); await controller.openAllDebridLoginWindow();
}); });
ipcMain.handle(IPC_CHANNELS.IMPORT_BESTDEBRID_COOKIES, async () => { ipcMain.handle(IPC_CHANNELS.OPEN_BESTDEBRID_LOGIN, async () => {
const options = { await controller.openBestDebridLoginWindow();
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 () => { ipcMain.handle(IPC_CHANNELS.GET_ALLDEBRID_HOST_INFO, async () => {

View File

@ -54,7 +54,7 @@ const api: ElectronApi = {
openSessionLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_SESSION_LOG), openSessionLog: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_SESSION_LOG),
openRealDebridLogin: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_REALDEBRID_LOGIN), openRealDebridLogin: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_REALDEBRID_LOGIN),
openAllDebridLogin: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_ALLDEBRID_LOGIN), openAllDebridLogin: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_ALLDEBRID_LOGIN),
importBestDebridCookies: (): Promise<number> => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_BESTDEBRID_COOKIES), openBestDebridLogin: (): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.OPEN_BESTDEBRID_LOGIN),
getAllDebridHostInfo: (): Promise<AllDebridHostInfo> => ipcRenderer.invoke(IPC_CHANNELS.GET_ALLDEBRID_HOST_INFO), getAllDebridHostInfo: (): Promise<AllDebridHostInfo> => ipcRenderer.invoke(IPC_CHANNELS.GET_ALLDEBRID_HOST_INFO),
retryExtraction: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.RETRY_EXTRACTION, packageId), retryExtraction: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.RETRY_EXTRACTION, packageId),
extractNow: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.EXTRACT_NOW, packageId), extractNow: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.EXTRACT_NOW, packageId),

View File

@ -1156,17 +1156,13 @@ export function App(): ReactElement {
}); });
}; };
const onImportBestDebridCookies = async (): Promise<void> => { const onOpenBestDebridLogin = async (): Promise<void> => {
await performQuickAction(async () => { await performQuickAction(async () => {
await persistDraftSettings(); await persistDraftSettings();
const count = await window.rd.importBestDebridCookies(); await window.rd.openBestDebridLogin();
if (count > 0) { showToast("BestDebrid Login-Fenster geöffnet", 2200);
showToast(`${count} BestDebrid-Cookies importiert`, 2200);
} else {
showToast("Keine Cookie-Datei ausgewählt", 2200);
}
}, (error) => { }, (error) => {
showToast(`BestDebrid Cookie-Import fehlgeschlagen: ${String(error)}`, 2800); showToast(`BestDebrid Login fehlgeschlagen: ${String(error)}`, 2800);
}); });
}; };
@ -2858,11 +2854,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 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> <label>BestDebrid API Token</label>
<input type="password" value={settingsDraft.bestToken} onChange={(e) => setText("bestToken", e.target.value)} /> <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 Cookie-Import statt API-Token verwenden</label> <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>
{settingsDraft.bestDebridUseWebLogin && ( {settingsDraft.bestDebridUseWebLogin && (
<> <>
<div className="hint">Exportiere deine BestDebrid-Cookies als Netscape-Textdatei (z.B. mit der Browser-Extension &quot;Get cookies.txt LOCALLY&quot;) und importiere sie hier.</div> <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 onImportBestDebridCookies(); }}>BestDebrid Cookies importieren</button> <button className="btn" disabled={actionBusy} onClick={() => { void onOpenBestDebridLogin(); }}>BestDebrid Web-Login öffnen</button>
</> </>
)} )}
<label>AllDebrid API Key</label> <label>AllDebrid API Key</label>

View File

@ -36,7 +36,7 @@ export const IPC_CHANNELS = {
OPEN_SESSION_LOG: "app:open-session-log", OPEN_SESSION_LOG: "app:open-session-log",
OPEN_REALDEBRID_LOGIN: "app:open-realdebrid-login", OPEN_REALDEBRID_LOGIN: "app:open-realdebrid-login",
OPEN_ALLDEBRID_LOGIN: "app:open-alldebrid-login", OPEN_ALLDEBRID_LOGIN: "app:open-alldebrid-login",
IMPORT_BESTDEBRID_COOKIES: "app:import-bestdebrid-cookies", OPEN_BESTDEBRID_LOGIN: "app:open-bestdebrid-login",
GET_ALLDEBRID_HOST_INFO: "app:get-alldebrid-host-info", GET_ALLDEBRID_HOST_INFO: "app:get-alldebrid-host-info",
RETRY_EXTRACTION: "queue:retry-extraction", RETRY_EXTRACTION: "queue:retry-extraction",
EXTRACT_NOW: "queue:extract-now", EXTRACT_NOW: "queue:extract-now",

View File

@ -49,7 +49,7 @@ export interface ElectronApi {
openSessionLog: () => Promise<void>; openSessionLog: () => Promise<void>;
openRealDebridLogin: () => Promise<void>; openRealDebridLogin: () => Promise<void>;
openAllDebridLogin: () => Promise<void>; openAllDebridLogin: () => Promise<void>;
importBestDebridCookies: () => Promise<number>; openBestDebridLogin: () => Promise<void>;
getAllDebridHostInfo: () => Promise<AllDebridHostInfo>; getAllDebridHostInfo: () => Promise<AllDebridHostInfo>;
retryExtraction: (packageId: string) => Promise<void>; retryExtraction: (packageId: string) => Promise<void>;
extractNow: (packageId: string) => Promise<void>; extractNow: (packageId: string) => Promise<void>;