♻️ 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();
|
await this.allDebridWebFallback.openLoginWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async openBestDebridLoginWindow(): Promise<void> {
|
public async importBestDebridCookies(filePath: string): Promise<number> {
|
||||||
await this.bestDebridWebFallback.openLoginWindow();
|
return this.bestDebridWebFallback.importCookiesFromFile(filePath);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAllDebridHostInfo(host = "rapidgator"): Promise<AllDebridHostInfo> {
|
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 { 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_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_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");
|
||||||
}
|
}
|
||||||
@ -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 {
|
function parseJson(text: string): Record<string, unknown> | null {
|
||||||
try {
|
try {
|
||||||
const parsed = JSON.parse(text) as unknown;
|
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 {
|
export class BestDebridWebFallback {
|
||||||
private queue: Promise<unknown> = Promise.resolve();
|
private queue: Promise<unknown> = Promise.resolve();
|
||||||
|
|
||||||
private loginWindow: BrowserWindow | null = null;
|
private cookiesImported = false;
|
||||||
|
|
||||||
private loginWindowPartition = "";
|
|
||||||
|
|
||||||
private getRememberSession: () => boolean;
|
private getRememberSession: () => boolean;
|
||||||
|
|
||||||
@ -86,32 +87,60 @@ 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, 10 * 60 * 1000);
|
const overallSignal = withTimeoutSignal(signal, 60_000);
|
||||||
return this.runExclusive(async () => {
|
return this.runExclusive(async () => {
|
||||||
throwIfAborted(overallSignal);
|
throwIfAborted(overallSignal);
|
||||||
if (!String(link || "").trim()) {
|
if (!String(link || "").trim()) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initial = await this.generate(link, overallSignal);
|
if (!this.cookiesImported) {
|
||||||
if (initial.kind === "success") {
|
throw new Error("BestDebrid: Keine Cookies importiert. Bitte zuerst über Einstellungen eine Cookie-Datei importieren.");
|
||||||
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 openLoginWindow(): Promise<void> {
|
public async importCookiesFromFile(filePath: string): Promise<number> {
|
||||||
const window = await this.ensureLoginWindow();
|
const text = fs.readFileSync(filePath, "utf-8");
|
||||||
if (window.isMinimized()) {
|
const cookies = parseNetscapeCookieFile(text);
|
||||||
window.restore();
|
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> {
|
public async clearSessions(): Promise<void> {
|
||||||
this.disposeLoginWindow();
|
this.cookiesImported = false;
|
||||||
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 {
|
||||||
@ -130,22 +159,13 @@ export class BestDebridWebFallback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public dispose(): void {
|
public dispose(): void {
|
||||||
this.disposeLoginWindow();
|
// nothing to clean up
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
||||||
@ -162,72 +182,7 @@ export class BestDebridWebFallback {
|
|||||||
return run;
|
return run;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async ensureLoginWindow(): Promise<BrowserWindow> {
|
private async generate(link: string, signal?: AbortSignal): Promise<{ kind: "success"; value: UnrestrictedLink } | { kind: "login_required" }> {
|
||||||
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, {
|
||||||
@ -236,7 +191,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_LOGIN_URL,
|
Referer: BESTDEBRID_DOWNLOADER_URL,
|
||||||
"User-Agent": BESTDEBRID_USER_AGENT,
|
"User-Agent": BESTDEBRID_USER_AGENT,
|
||||||
"X-Requested-With": "XMLHttpRequest"
|
"X-Requested-With": "XMLHttpRequest"
|
||||||
},
|
},
|
||||||
@ -246,7 +201,6 @@ 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" };
|
||||||
}
|
}
|
||||||
@ -259,9 +213,7 @@ 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" };
|
||||||
}
|
}
|
||||||
@ -277,11 +229,10 @@ 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) {
|
||||||
// Size might be like "96.63 MB" — parse it
|
const match = fileSizeRaw.match(/([\d.]+)\s*(KB|KiB|MB|MiB|GB|GiB|TB|TiB|B)/i);
|
||||||
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();
|
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 };
|
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));
|
||||||
}
|
}
|
||||||
@ -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();
|
item.updatedAt = nowMs();
|
||||||
this.emitState();
|
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;
|
active.resumable = result.resumable;
|
||||||
if (!active.resumable && !active.nonResumableCounted) {
|
if (!active.resumable && !active.nonResumableCounted) {
|
||||||
active.nonResumableCounted = true;
|
active.nonResumableCounted = true;
|
||||||
@ -5176,8 +5176,10 @@ 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");
|
||||||
@ -5426,7 +5428,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 (${pLabel})`;
|
item.fullStatus = `Warte auf Festplatte (${label})`;
|
||||||
item.updatedAt = nowTick;
|
item.updatedAt = nowTick;
|
||||||
this.emitState();
|
this.emitState();
|
||||||
lastDiskBusyEmitAt = nowTick;
|
lastDiskBusyEmitAt = nowTick;
|
||||||
@ -5537,7 +5539,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 (${pLabel})`;
|
item.fullStatus = `Warte auf Festplatte (${label})`;
|
||||||
item.updatedAt = nowTick;
|
item.updatedAt = nowTick;
|
||||||
this.emitState();
|
this.emitState();
|
||||||
lastIdleEmitAt = nowTick;
|
lastIdleEmitAt = nowTick;
|
||||||
@ -5553,7 +5555,7 @@ export class DownloadManager extends EventEmitter {
|
|||||||
}
|
}
|
||||||
item.status = "downloading";
|
item.status = "downloading";
|
||||||
item.speedBps = 0;
|
item.speedBps = 0;
|
||||||
item.fullStatus = `Warte auf Daten (${pLabel})`;
|
item.fullStatus = `Warte auf Daten (${label})`;
|
||||||
if (nowTick - lastIdleEmitAt >= idlePulseMs) {
|
if (nowTick - lastIdleEmitAt >= idlePulseMs) {
|
||||||
item.updatedAt = nowTick;
|
item.updatedAt = nowTick;
|
||||||
this.emitState();
|
this.emitState();
|
||||||
@ -5662,7 +5664,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 (${pLabel})`;
|
item.fullStatus = `Warte auf Festplatte (${label})`;
|
||||||
item.updatedAt = nowTick;
|
item.updatedAt = nowTick;
|
||||||
this.emitState();
|
this.emitState();
|
||||||
lastDiskBusyEmitAt = nowTick;
|
lastDiskBusyEmitAt = nowTick;
|
||||||
@ -5706,10 +5708,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 (${pLabel})`;
|
item.fullStatus = `Warte auf Festplatte (${label})`;
|
||||||
} else {
|
} else {
|
||||||
item.speedBps = Math.max(0, Math.floor(speed));
|
item.speedBps = Math.max(0, Math.floor(speed));
|
||||||
item.fullStatus = `Download läuft (${pLabel})`;
|
item.fullStatus = `Download läuft (${label})`;
|
||||||
}
|
}
|
||||||
const nowTick = nowMs();
|
const nowTick = nowMs();
|
||||||
if (nowTick - lastUiEmitAt >= uiUpdateIntervalMs) {
|
if (nowTick - lastUiEmitAt >= uiUpdateIntervalMs) {
|
||||||
|
|||||||
@ -454,8 +454,19 @@ function registerIpcHandlers(): void {
|
|||||||
await controller.openAllDebridLoginWindow();
|
await controller.openAllDebridLoginWindow();
|
||||||
});
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.OPEN_BESTDEBRID_LOGIN, async () => {
|
ipcMain.handle(IPC_CHANNELS.IMPORT_BESTDEBRID_COOKIES, async () => {
|
||||||
await controller.openBestDebridLoginWindow();
|
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 () => {
|
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),
|
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),
|
||||||
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),
|
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),
|
||||||
|
|||||||
@ -1156,13 +1156,17 @@ export function App(): ReactElement {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onOpenBestDebridLogin = async (): Promise<void> => {
|
const onImportBestDebridCookies = async (): Promise<void> => {
|
||||||
await performQuickAction(async () => {
|
await performQuickAction(async () => {
|
||||||
await persistDraftSettings();
|
await persistDraftSettings();
|
||||||
await window.rd.openBestDebridLogin();
|
const count = await window.rd.importBestDebridCookies();
|
||||||
showToast("BestDebrid Login-Fenster geöffnet", 2200);
|
if (count > 0) {
|
||||||
|
showToast(`${count} BestDebrid-Cookies importiert`, 2200);
|
||||||
|
} else {
|
||||||
|
showToast("Keine Cookie-Datei ausgewählt", 2200);
|
||||||
|
}
|
||||||
}, (error) => {
|
}, (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 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 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 && (
|
{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>
|
<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 onOpenBestDebridLogin(); }}>BestDebrid Web-Login öffnen</button>
|
<button className="btn" disabled={actionBusy} onClick={() => { void onImportBestDebridCookies(); }}>BestDebrid Cookies importieren</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<label>AllDebrid API Key</label>
|
<label>AllDebrid API Key</label>
|
||||||
|
|||||||
@ -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",
|
||||||
OPEN_BESTDEBRID_LOGIN: "app:open-bestdebrid-login",
|
IMPORT_BESTDEBRID_COOKIES: "app:import-bestdebrid-cookies",
|
||||||
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",
|
||||||
|
|||||||
@ -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>;
|
||||||
openBestDebridLogin: () => Promise<void>;
|
importBestDebridCookies: () => Promise<number>;
|
||||||
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>;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user