✨ feat(bestdebrid): add BestDebrid web-login provider
- New BestDebrid web-login option (BrowserWindow + session.fetch) - Uses bestdebrid.com/api/v1/generateLink with browser session cookies - Login via BestDebrid website in embedded browser window - Toggle "BestDebrid per Web-Login statt API-Token verwenden" - Provider label shows "BestDebrid (Web)" or "BestDebrid (API)" - Session persistence respects "Token merken" setting Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
54e502b9af
commit
438a9f209e
@ -23,6 +23,7 @@ import { fetchAllDebridHostInfo } from "./debrid";
|
|||||||
import { parseCollectorInput } from "./link-parser";
|
import { parseCollectorInput } from "./link-parser";
|
||||||
import { configureLogger, getLogFilePath, logger } from "./logger";
|
import { configureLogger, getLogFilePath, logger } from "./logger";
|
||||||
import { AllDebridWebFallback } from "./all-debrid-web";
|
import { AllDebridWebFallback } from "./all-debrid-web";
|
||||||
|
import { BestDebridWebFallback } from "./bestdebrid-web";
|
||||||
import { RealDebridWebFallback } from "./realdebrid-web";
|
import { RealDebridWebFallback } from "./realdebrid-web";
|
||||||
import { initSessionLog, getSessionLogPath, shutdownSessionLog } from "./session-log";
|
import { initSessionLog, getSessionLogPath, shutdownSessionLog } from "./session-log";
|
||||||
import { MegaWebFallback } from "./mega-web-fallback";
|
import { MegaWebFallback } from "./mega-web-fallback";
|
||||||
@ -50,6 +51,8 @@ export class AppController {
|
|||||||
|
|
||||||
private allDebridWebFallback: AllDebridWebFallback;
|
private allDebridWebFallback: AllDebridWebFallback;
|
||||||
|
|
||||||
|
private bestDebridWebFallback: BestDebridWebFallback;
|
||||||
|
|
||||||
private lastUpdateCheck: UpdateCheckResult | null = null;
|
private lastUpdateCheck: UpdateCheckResult | null = null;
|
||||||
|
|
||||||
private lastUpdateCheckAt = 0;
|
private lastUpdateCheckAt = 0;
|
||||||
@ -71,10 +74,12 @@ export class AppController {
|
|||||||
}));
|
}));
|
||||||
this.realDebridWebFallback = new RealDebridWebFallback(() => this.settings.rememberToken);
|
this.realDebridWebFallback = new RealDebridWebFallback(() => this.settings.rememberToken);
|
||||||
this.allDebridWebFallback = new AllDebridWebFallback(() => this.settings.rememberToken);
|
this.allDebridWebFallback = new AllDebridWebFallback(() => this.settings.rememberToken);
|
||||||
|
this.bestDebridWebFallback = new BestDebridWebFallback(() => this.settings.rememberToken);
|
||||||
this.manager = new DownloadManager(this.settings, session, this.storagePaths, {
|
this.manager = new DownloadManager(this.settings, session, this.storagePaths, {
|
||||||
megaWebUnrestrict: (link: string, signal?: AbortSignal) => this.megaWebFallback.unrestrict(link, signal),
|
megaWebUnrestrict: (link: string, signal?: AbortSignal) => this.megaWebFallback.unrestrict(link, signal),
|
||||||
allDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.allDebridWebFallback.unrestrict(link, signal),
|
allDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.allDebridWebFallback.unrestrict(link, signal),
|
||||||
realDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.realDebridWebFallback.unrestrict(link, signal),
|
realDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.realDebridWebFallback.unrestrict(link, signal),
|
||||||
|
bestDebridWebUnrestrict: (link: string, signal?: AbortSignal) => this.bestDebridWebFallback.unrestrict(link, signal),
|
||||||
invalidateMegaSession: () => this.megaWebFallback.invalidateSession(),
|
invalidateMegaSession: () => this.megaWebFallback.invalidateSession(),
|
||||||
onHistoryEntry: (entry: HistoryEntry) => {
|
onHistoryEntry: (entry: HistoryEntry) => {
|
||||||
addHistoryEntry(this.storagePaths, entry);
|
addHistoryEntry(this.storagePaths, entry);
|
||||||
@ -117,6 +122,7 @@ export class AppController {
|
|||||||
|| settings.realDebridUseWebLogin
|
|| settings.realDebridUseWebLogin
|
||||||
|| (settings.megaLogin.trim() && settings.megaPassword.trim())
|
|| (settings.megaLogin.trim() && settings.megaPassword.trim())
|
||||||
|| settings.bestToken.trim()
|
|| settings.bestToken.trim()
|
||||||
|
|| settings.bestDebridUseWebLogin
|
||||||
|| settings.allDebridUseWebLogin
|
|| settings.allDebridUseWebLogin
|
||||||
|| settings.allDebridToken.trim()
|
|| settings.allDebridToken.trim()
|
||||||
|| (settings.ddownloadLogin.trim() && settings.ddownloadPassword.trim())
|
|| (settings.ddownloadLogin.trim() && settings.ddownloadPassword.trim())
|
||||||
@ -180,6 +186,9 @@ export class AppController {
|
|||||||
void this.allDebridWebFallback.clearSessions().catch((error) => {
|
void this.allDebridWebFallback.clearSessions().catch((error) => {
|
||||||
logger.warn(`AllDebrid Web-Session konnte nicht gelöscht werden: ${String(error)}`);
|
logger.warn(`AllDebrid Web-Session konnte nicht gelöscht werden: ${String(error)}`);
|
||||||
});
|
});
|
||||||
|
void this.bestDebridWebFallback.clearSessions().catch((error) => {
|
||||||
|
logger.warn(`BestDebrid Web-Session konnte nicht gelöscht werden: ${String(error)}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return this.settings;
|
return this.settings;
|
||||||
}
|
}
|
||||||
@ -192,6 +201,10 @@ export class AppController {
|
|||||||
await this.allDebridWebFallback.openLoginWindow();
|
await this.allDebridWebFallback.openLoginWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async openBestDebridLoginWindow(): Promise<void> {
|
||||||
|
await this.bestDebridWebFallback.openLoginWindow();
|
||||||
|
}
|
||||||
|
|
||||||
public async getAllDebridHostInfo(host = "rapidgator"): Promise<AllDebridHostInfo> {
|
public async getAllDebridHostInfo(host = "rapidgator"): Promise<AllDebridHostInfo> {
|
||||||
if (this.settings.allDebridUseWebLogin) {
|
if (this.settings.allDebridUseWebLogin) {
|
||||||
return this.allDebridWebFallback.getHostInfo(host);
|
return this.allDebridWebFallback.getHostInfo(host);
|
||||||
@ -394,6 +407,7 @@ export class AppController {
|
|||||||
this.megaWebFallback.dispose();
|
this.megaWebFallback.dispose();
|
||||||
this.realDebridWebFallback.dispose();
|
this.realDebridWebFallback.dispose();
|
||||||
this.allDebridWebFallback.dispose();
|
this.allDebridWebFallback.dispose();
|
||||||
|
this.bestDebridWebFallback.dispose();
|
||||||
shutdownSessionLog();
|
shutdownSessionLog();
|
||||||
logger.info("App beendet");
|
logger.info("App beendet");
|
||||||
}
|
}
|
||||||
|
|||||||
301
src/main/bestdebrid-web.ts
Normal file
301
src/main/bestdebrid-web.ts
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
import { BrowserWindow, session } from "electron";
|
||||||
|
import { UnrestrictedLink } from "./realdebrid";
|
||||||
|
import { filenameFromUrl, sleep } from "./utils";
|
||||||
|
|
||||||
|
const BESTDEBRID_BASE_URL = "https://bestdebrid.com";
|
||||||
|
const BESTDEBRID_LOGIN_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");
|
||||||
|
}
|
||||||
|
|
||||||
|
function withTimeoutSignal(signal: AbortSignal | undefined, timeoutMs: number): AbortSignal {
|
||||||
|
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
||||||
|
if (!signal) {
|
||||||
|
return timeoutSignal;
|
||||||
|
}
|
||||||
|
return AbortSignal.any([signal, timeoutSignal]);
|
||||||
|
}
|
||||||
|
|
||||||
|
function throwIfAborted(signal?: AbortSignal): void {
|
||||||
|
if (signal?.aborted) {
|
||||||
|
throw abortError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return parsed as Record<string, unknown>;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class BestDebridWebFallback {
|
||||||
|
private queue: Promise<unknown> = Promise.resolve();
|
||||||
|
|
||||||
|
private loginWindow: BrowserWindow | null = null;
|
||||||
|
|
||||||
|
private loginWindowPartition = "";
|
||||||
|
|
||||||
|
private getRememberSession: () => boolean;
|
||||||
|
|
||||||
|
public constructor(getRememberSession: () => boolean) {
|
||||||
|
this.getRememberSession = getRememberSession;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async unrestrict(link: string, signal?: AbortSignal): Promise<UnrestrictedLink | null> {
|
||||||
|
const overallSignal = withTimeoutSignal(signal, 10 * 60 * 1000);
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
return this.waitForLoginAndGenerate(link, overallSignal);
|
||||||
|
}, overallSignal);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async openLoginWindow(): Promise<void> {
|
||||||
|
const window = await this.ensureLoginWindow();
|
||||||
|
if (window.isMinimized()) {
|
||||||
|
window.restore();
|
||||||
|
}
|
||||||
|
window.show();
|
||||||
|
window.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async clearSessions(): Promise<void> {
|
||||||
|
this.disposeLoginWindow();
|
||||||
|
for (const partition of [BESTDEBRID_PERSISTENT_PARTITION, BESTDEBRID_TRANSIENT_PARTITION]) {
|
||||||
|
const currentSession = session.fromPartition(partition);
|
||||||
|
try {
|
||||||
|
await currentSession.clearStorageData({
|
||||||
|
storages: ["cookies", "indexdb", "localstorage", "serviceworkers", "cachestorage"]
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await currentSession.clearCache();
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
this.disposeLoginWindow();
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
const guardedJob = async (): Promise<T> => {
|
||||||
|
throwIfAborted(signal);
|
||||||
|
const waited = Date.now() - queuedAt;
|
||||||
|
if (waited > queueWaitTimeoutMs) {
|
||||||
|
throw new Error(`BestDebrid-Web Queue-Timeout (${Math.floor(waited / 1000)}s gewartet)`);
|
||||||
|
}
|
||||||
|
return job();
|
||||||
|
};
|
||||||
|
const run = this.queue.then(guardedJob, guardedJob);
|
||||||
|
this.queue = run.then(() => undefined, () => undefined);
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
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.setMenuBarVisibility(false);
|
||||||
|
window.on("closed", () => {
|
||||||
|
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);
|
||||||
|
const currentSession = session.fromPartition(this.getPartition());
|
||||||
|
const response = await currentSession.fetch(BESTDEBRID_GENERATE_URL, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
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,
|
||||||
|
"User-Agent": BESTDEBRID_USER_AGENT,
|
||||||
|
"X-Requested-With": "XMLHttpRequest"
|
||||||
|
},
|
||||||
|
body: new URLSearchParams({ link, pass: "", boxlinklist: "" }).toString(),
|
||||||
|
signal: withTimeoutSignal(signal, 30_000)
|
||||||
|
});
|
||||||
|
|
||||||
|
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" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = parseJson(text.trim());
|
||||||
|
if (!payload) {
|
||||||
|
return { kind: "login_required" };
|
||||||
|
}
|
||||||
|
|
||||||
|
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" };
|
||||||
|
}
|
||||||
|
throw new Error(`BestDebrid Web: ${message || "Unbekannter Fehler"}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const directUrl = String(payload.link || "").trim();
|
||||||
|
if (!directUrl) {
|
||||||
|
throw new Error("BestDebrid Web: Antwort ohne Download-Link");
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileName = String(payload.filename || "").trim() || filenameFromUrl(directUrl) || filenameFromUrl(link);
|
||||||
|
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);
|
||||||
|
if (match) {
|
||||||
|
const value = parseFloat(match[1]);
|
||||||
|
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 };
|
||||||
|
fileSize = Math.floor(value * (multipliers[unit] || 1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: "success",
|
||||||
|
value: {
|
||||||
|
directUrl,
|
||||||
|
fileName,
|
||||||
|
fileSize,
|
||||||
|
retriesUsed: 0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -46,6 +46,7 @@ export function defaultSettings(): AppSettings {
|
|||||||
megaPassword: "",
|
megaPassword: "",
|
||||||
megaDebridPreferApi: true,
|
megaDebridPreferApi: true,
|
||||||
bestToken: "",
|
bestToken: "",
|
||||||
|
bestDebridUseWebLogin: false,
|
||||||
allDebridToken: "",
|
allDebridToken: "",
|
||||||
allDebridUseWebLogin: false,
|
allDebridUseWebLogin: false,
|
||||||
ddownloadLogin: "",
|
ddownloadLogin: "",
|
||||||
|
|||||||
@ -34,11 +34,13 @@ interface ProviderUnrestrictedLink extends UnrestrictedLink {
|
|||||||
export type MegaWebUnrestrictor = (link: string, signal?: AbortSignal) => Promise<UnrestrictedLink | null>;
|
export type MegaWebUnrestrictor = (link: string, signal?: AbortSignal) => Promise<UnrestrictedLink | null>;
|
||||||
export type AllDebridWebUnrestrictor = (link: string, signal?: AbortSignal) => Promise<UnrestrictedLink | null>;
|
export type AllDebridWebUnrestrictor = (link: string, signal?: AbortSignal) => Promise<UnrestrictedLink | null>;
|
||||||
export type RealDebridWebUnrestrictor = (link: string, signal?: AbortSignal) => Promise<UnrestrictedLink | null>;
|
export type RealDebridWebUnrestrictor = (link: string, signal?: AbortSignal) => Promise<UnrestrictedLink | null>;
|
||||||
|
export type BestDebridWebUnrestrictor = (link: string, signal?: AbortSignal) => Promise<UnrestrictedLink | null>;
|
||||||
|
|
||||||
interface DebridServiceOptions {
|
interface DebridServiceOptions {
|
||||||
megaWebUnrestrict?: MegaWebUnrestrictor;
|
megaWebUnrestrict?: MegaWebUnrestrictor;
|
||||||
allDebridWebUnrestrict?: AllDebridWebUnrestrictor;
|
allDebridWebUnrestrict?: AllDebridWebUnrestrictor;
|
||||||
realDebridWebUnrestrict?: RealDebridWebUnrestrictor;
|
realDebridWebUnrestrict?: RealDebridWebUnrestrictor;
|
||||||
|
bestDebridWebUnrestrict?: BestDebridWebUnrestrictor;
|
||||||
}
|
}
|
||||||
|
|
||||||
function cloneSettings(settings: AppSettings): AppSettings {
|
function cloneSettings(settings: AppSettings): AppSettings {
|
||||||
@ -1585,6 +1587,10 @@ export class DebridService {
|
|||||||
return Boolean(settings.allDebridUseWebLogin && this.options.allDebridWebUnrestrict);
|
return Boolean(settings.allDebridUseWebLogin && this.options.allDebridWebUnrestrict);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private shouldUseBestDebridWeb(settings: AppSettings): boolean {
|
||||||
|
return Boolean(settings.bestDebridUseWebLogin && this.options.bestDebridWebUnrestrict);
|
||||||
|
}
|
||||||
|
|
||||||
public async unrestrictLink(link: string, signal?: AbortSignal, settingsSnapshot?: AppSettings): Promise<ProviderUnrestrictedLink> {
|
public async unrestrictLink(link: string, signal?: AbortSignal, settingsSnapshot?: AppSettings): Promise<ProviderUnrestrictedLink> {
|
||||||
const settings = settingsSnapshot ? cloneSettings(settingsSnapshot) : cloneSettings(this.settings);
|
const settings = settingsSnapshot ? cloneSettings(settingsSnapshot) : cloneSettings(this.settings);
|
||||||
|
|
||||||
@ -1718,7 +1724,7 @@ export class DebridService {
|
|||||||
if (provider === "onefichier") {
|
if (provider === "onefichier") {
|
||||||
return Boolean(settings.oneFichierApiKey.trim());
|
return Boolean(settings.oneFichierApiKey.trim());
|
||||||
}
|
}
|
||||||
return Boolean(settings.bestToken.trim());
|
return Boolean(this.shouldUseBestDebridWeb(settings) || settings.bestToken.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
private async unrestrictViaProvider(settings: AppSettings, provider: DebridProvider, link: string, signal?: AbortSignal): Promise<UnrestrictedLink> {
|
private async unrestrictViaProvider(settings: AppSettings, provider: DebridProvider, link: string, signal?: AbortSignal): Promise<UnrestrictedLink> {
|
||||||
@ -1757,6 +1763,16 @@ export class DebridService {
|
|||||||
if (provider === "onefichier") {
|
if (provider === "onefichier") {
|
||||||
return new OneFichierClient(settings.oneFichierApiKey).unrestrictLink(link, signal);
|
return new OneFichierClient(settings.oneFichierApiKey).unrestrictLink(link, signal);
|
||||||
}
|
}
|
||||||
return new BestDebridClient(settings.bestToken).unrestrictLink(link, signal);
|
if (this.shouldUseBestDebridWeb(settings) && this.options.bestDebridWebUnrestrict) {
|
||||||
|
const bdResult = await this.options.bestDebridWebUnrestrict(link, signal);
|
||||||
|
if (!bdResult) {
|
||||||
|
throw new Error("BestDebrid-Web-Fallback nicht verfügbar");
|
||||||
|
}
|
||||||
|
bdResult.sourceLabel = "Web";
|
||||||
|
return bdResult;
|
||||||
|
}
|
||||||
|
const bdResult = await new BestDebridClient(settings.bestToken).unrestrictLink(link, signal);
|
||||||
|
bdResult.sourceLabel = "API";
|
||||||
|
return bdResult;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -38,7 +38,7 @@ function releaseTlsSkip(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
import { cleanupCancelledPackageArtifactsAsync, removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup";
|
import { cleanupCancelledPackageArtifactsAsync, removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup";
|
||||||
import { AllDebridWebUnrestrictor, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkRapidgatorOnline } from "./debrid";
|
import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkRapidgatorOnline } from "./debrid";
|
||||||
import { cleanupArchives, clearExtractResumeState, collectArchiveCleanupTargets, extractPackageArchives, findArchiveCandidates, hasAnyFilesRecursive, removeEmptyDirectoryTree } from "./extractor";
|
import { cleanupArchives, clearExtractResumeState, collectArchiveCleanupTargets, extractPackageArchives, findArchiveCandidates, hasAnyFilesRecursive, removeEmptyDirectoryTree } from "./extractor";
|
||||||
import { validateFileAgainstManifest } from "./integrity";
|
import { validateFileAgainstManifest } from "./integrity";
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
@ -159,6 +159,7 @@ type DownloadManagerOptions = {
|
|||||||
megaWebUnrestrict?: MegaWebUnrestrictor;
|
megaWebUnrestrict?: MegaWebUnrestrictor;
|
||||||
allDebridWebUnrestrict?: AllDebridWebUnrestrictor;
|
allDebridWebUnrestrict?: AllDebridWebUnrestrictor;
|
||||||
realDebridWebUnrestrict?: RealDebridWebUnrestrictor;
|
realDebridWebUnrestrict?: RealDebridWebUnrestrictor;
|
||||||
|
bestDebridWebUnrestrict?: BestDebridWebUnrestrictor;
|
||||||
invalidateMegaSession?: () => void;
|
invalidateMegaSession?: () => void;
|
||||||
onHistoryEntry?: HistoryEntryCallback;
|
onHistoryEntry?: HistoryEntryCallback;
|
||||||
};
|
};
|
||||||
@ -953,7 +954,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.debridService = new DebridService(settings, {
|
this.debridService = new DebridService(settings, {
|
||||||
megaWebUnrestrict: options.megaWebUnrestrict,
|
megaWebUnrestrict: options.megaWebUnrestrict,
|
||||||
allDebridWebUnrestrict: options.allDebridWebUnrestrict,
|
allDebridWebUnrestrict: options.allDebridWebUnrestrict,
|
||||||
realDebridWebUnrestrict: options.realDebridWebUnrestrict
|
realDebridWebUnrestrict: options.realDebridWebUnrestrict,
|
||||||
|
bestDebridWebUnrestrict: options.bestDebridWebUnrestrict
|
||||||
});
|
});
|
||||||
this.invalidateMegaSessionFn = options.invalidateMegaSession;
|
this.invalidateMegaSessionFn = options.invalidateMegaSession;
|
||||||
this.onHistoryEntryCallback = options.onHistoryEntry;
|
this.onHistoryEntryCallback = options.onHistoryEntry;
|
||||||
|
|||||||
@ -454,6 +454,10 @@ function registerIpcHandlers(): void {
|
|||||||
await controller.openAllDebridLoginWindow();
|
await controller.openAllDebridLoginWindow();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcMain.handle(IPC_CHANNELS.OPEN_BESTDEBRID_LOGIN, async () => {
|
||||||
|
await controller.openBestDebridLoginWindow();
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.handle(IPC_CHANNELS.GET_ALLDEBRID_HOST_INFO, async () => {
|
ipcMain.handle(IPC_CHANNELS.GET_ALLDEBRID_HOST_INFO, async () => {
|
||||||
return controller.getAllDebridHostInfo();
|
return controller.getAllDebridHostInfo();
|
||||||
});
|
});
|
||||||
|
|||||||
@ -112,6 +112,7 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
|||||||
megaPassword: asText(settings.megaPassword),
|
megaPassword: asText(settings.megaPassword),
|
||||||
megaDebridPreferApi: settings.megaDebridPreferApi !== undefined ? Boolean(settings.megaDebridPreferApi) : true,
|
megaDebridPreferApi: settings.megaDebridPreferApi !== undefined ? Boolean(settings.megaDebridPreferApi) : true,
|
||||||
bestToken: asText(settings.bestToken),
|
bestToken: asText(settings.bestToken),
|
||||||
|
bestDebridUseWebLogin: Boolean(settings.bestDebridUseWebLogin),
|
||||||
allDebridToken: asText(settings.allDebridToken),
|
allDebridToken: asText(settings.allDebridToken),
|
||||||
allDebridUseWebLogin: Boolean(settings.allDebridUseWebLogin),
|
allDebridUseWebLogin: Boolean(settings.allDebridUseWebLogin),
|
||||||
ddownloadLogin: asText(settings.ddownloadLogin),
|
ddownloadLogin: asText(settings.ddownloadLogin),
|
||||||
@ -207,6 +208,7 @@ function sanitizeCredentialPersistence(settings: AppSettings): AppSettings {
|
|||||||
megaLogin: "",
|
megaLogin: "",
|
||||||
megaPassword: "",
|
megaPassword: "",
|
||||||
bestToken: "",
|
bestToken: "",
|
||||||
|
bestDebridUseWebLogin: settings.bestDebridUseWebLogin,
|
||||||
allDebridToken: "",
|
allDebridToken: "",
|
||||||
ddownloadLogin: "",
|
ddownloadLogin: "",
|
||||||
ddownloadPassword: "",
|
ddownloadPassword: "",
|
||||||
|
|||||||
@ -54,6 +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),
|
||||||
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),
|
||||||
|
|||||||
@ -63,7 +63,7 @@ const emptyStats = (): DownloadStats => ({
|
|||||||
|
|
||||||
const emptySnapshot = (): UiSnapshot => ({
|
const emptySnapshot = (): UiSnapshot => ({
|
||||||
settings: {
|
settings: {
|
||||||
token: "", realDebridUseWebLogin: false, megaLogin: "", megaPassword: "", megaDebridPreferApi: true, bestToken: "", allDebridToken: "", allDebridUseWebLogin: false, ddownloadLogin: "", ddownloadPassword: "", oneFichierApiKey: "",
|
token: "", realDebridUseWebLogin: false, megaLogin: "", megaPassword: "", megaDebridPreferApi: true, bestToken: "", bestDebridUseWebLogin: false, allDebridToken: "", allDebridUseWebLogin: false, ddownloadLogin: "", ddownloadPassword: "", oneFichierApiKey: "",
|
||||||
archivePasswordList: "",
|
archivePasswordList: "",
|
||||||
rememberToken: true, providerPrimary: "realdebrid", providerSecondary: "megadebrid",
|
rememberToken: true, providerPrimary: "realdebrid", providerSecondary: "megadebrid",
|
||||||
providerTertiary: "bestdebrid", autoProviderFallback: true, outputDir: "", packageName: "",
|
providerTertiary: "bestdebrid", autoProviderFallback: true, outputDir: "", packageName: "",
|
||||||
@ -988,14 +988,14 @@ export function App(): ReactElement {
|
|||||||
if (settingsDraft.megaLogin.trim() && settingsDraft.megaPassword.trim()) {
|
if (settingsDraft.megaLogin.trim() && settingsDraft.megaPassword.trim()) {
|
||||||
list.push("megadebrid");
|
list.push("megadebrid");
|
||||||
}
|
}
|
||||||
if (settingsDraft.bestToken.trim()) {
|
if (settingsDraft.bestDebridUseWebLogin || settingsDraft.bestToken.trim()) {
|
||||||
list.push("bestdebrid");
|
list.push("bestdebrid");
|
||||||
}
|
}
|
||||||
if (settingsDraft.allDebridUseWebLogin || settingsDraft.allDebridToken.trim()) {
|
if (settingsDraft.allDebridUseWebLogin || settingsDraft.allDebridToken.trim()) {
|
||||||
list.push("alldebrid");
|
list.push("alldebrid");
|
||||||
}
|
}
|
||||||
return list;
|
return list;
|
||||||
}, [settingsDraft.token, settingsDraft.realDebridUseWebLogin, settingsDraft.megaLogin, settingsDraft.megaPassword, settingsDraft.bestToken, settingsDraft.allDebridToken, settingsDraft.allDebridUseWebLogin]);
|
}, [settingsDraft.token, settingsDraft.realDebridUseWebLogin, settingsDraft.megaLogin, settingsDraft.megaPassword, settingsDraft.bestToken, settingsDraft.bestDebridUseWebLogin, settingsDraft.allDebridToken, settingsDraft.allDebridUseWebLogin]);
|
||||||
|
|
||||||
// DDownload is a direct file hoster (not a debrid service) and is used automatically
|
// DDownload is a direct file hoster (not a debrid service) and is used automatically
|
||||||
// for ddownload.com/ddl.to URLs. It counts as a configured account but does not
|
// for ddownload.com/ddl.to URLs. It counts as a configured account but does not
|
||||||
@ -1156,6 +1156,16 @@ export function App(): ReactElement {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onOpenBestDebridLogin = async (): Promise<void> => {
|
||||||
|
await performQuickAction(async () => {
|
||||||
|
await persistDraftSettings();
|
||||||
|
await window.rd.openBestDebridLogin();
|
||||||
|
showToast("BestDebrid Login-Fenster geöffnet", 2200);
|
||||||
|
}, (error) => {
|
||||||
|
showToast(`BestDebrid Login fehlgeschlagen: ${String(error)}`, 2800);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const onCheckUpdates = async (): Promise<void> => {
|
const onCheckUpdates = async (): Promise<void> => {
|
||||||
let updateResult: UpdateCheckResult | null = null;
|
let updateResult: UpdateCheckResult | null = null;
|
||||||
await performQuickAction(async () => {
|
await performQuickAction(async () => {
|
||||||
@ -2844,6 +2854,13 @@ 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>
|
||||||
|
{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>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
<label>AllDebrid API Key</label>
|
<label>AllDebrid API Key</label>
|
||||||
<input type="password" value={settingsDraft.allDebridToken} onChange={(e) => setText("allDebridToken", e.target.value)} />
|
<input type="password" value={settingsDraft.allDebridToken} onChange={(e) => setText("allDebridToken", e.target.value)} />
|
||||||
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.allDebridUseWebLogin} onChange={(e) => setBool("allDebridUseWebLogin", e.target.checked)} /> AllDebrid per Web-Login statt API-Key verwenden</label>
|
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.allDebridUseWebLogin} onChange={(e) => setBool("allDebridUseWebLogin", e.target.checked)} /> AllDebrid per Web-Login statt API-Key verwenden</label>
|
||||||
|
|||||||
@ -36,6 +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",
|
||||||
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,6 +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>;
|
||||||
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>;
|
||||||
|
|||||||
@ -43,6 +43,7 @@ export interface AppSettings {
|
|||||||
megaPassword: string;
|
megaPassword: string;
|
||||||
megaDebridPreferApi: boolean;
|
megaDebridPreferApi: boolean;
|
||||||
bestToken: string;
|
bestToken: string;
|
||||||
|
bestDebridUseWebLogin: boolean;
|
||||||
allDebridToken: string;
|
allDebridToken: string;
|
||||||
allDebridUseWebLogin: boolean;
|
allDebridUseWebLogin: boolean;
|
||||||
ddownloadLogin: string;
|
ddownloadLogin: string;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user