diff --git a/src/main/constants.ts b/src/main/constants.ts index 97d46de..55ba356 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -52,6 +52,7 @@ export function defaultSettings(): AppSettings { ddownloadLogin: "", ddownloadPassword: "", oneFichierApiKey: "", + debridLinkApiKeys: "", archivePasswordList: "", rememberToken: true, providerPrimary: "realdebrid", diff --git a/src/main/debrid.ts b/src/main/debrid.ts index c84f5be..1820ae4 100644 --- a/src/main/debrid.ts +++ b/src/main/debrid.ts @@ -17,13 +17,17 @@ const MEGA_DEBRID_API_BASE = "https://www.mega-debrid.eu/api.php"; const ONEFICHIER_API_BASE = "https://api.1fichier.com/v1"; const ONEFICHIER_URL_RE = /^https?:\/\/(?:www\.)?(?:1fichier\.com|alterupload\.com|cjoint\.net|desfichiers\.com|dfichiers\.com|megadl\.fr|mesfichiers\.org|piecejointe\.net|pjointe\.com|tenvoi\.com|dl4free\.com)\/\?([a-z0-9]{5,20})$/i; +const DEBRID_LINK_API_BASE = "https://debrid-link.com/api/v2"; +const DEBRID_LINK_QUOTA_ERRORS = new Set(["maxLink", "maxLinkHost", "maxData", "maxDataHost", "maxAttempts", "maxTransfer"]); + const PROVIDER_LABELS: Record = { realdebrid: "Real-Debrid", megadebrid: "Mega-Debrid", bestdebrid: "BestDebrid", alldebrid: "AllDebrid", ddownload: "DDownload", - onefichier: "1Fichier" + onefichier: "1Fichier", + debridlink: "Debrid-Link" }; interface ProviderUnrestrictedLink extends UnrestrictedLink { @@ -1252,6 +1256,111 @@ export async function fetchAllDebridHostInfo(token: string, host = "rapidgator", return new AllDebridClient(token).getHostInfo(host, signal); } +// ── Debrid-Link Client ── + +class DebridLinkClient { + private apiKeys: string[]; + private currentKeyIndex: number = 0; + + public constructor(apiKeysRaw: string) { + this.apiKeys = apiKeysRaw + .split(/[\n,]+/) + .map((k) => k.trim()) + .filter(Boolean); + } + + public async unrestrictLink(link: string, signal?: AbortSignal): Promise { + if (this.apiKeys.length === 0) { + throw new Error("Debrid-Link: Kein API-Key konfiguriert"); + } + + const startIndex = this.currentKeyIndex; + let triedAll = false; + + while (!triedAll) { + const apiKey = this.apiKeys[this.currentKeyIndex]; + const keyLabel = this.apiKeys.length > 1 ? ` (Key ${this.currentKeyIndex + 1}/${this.apiKeys.length})` : ""; + + let lastError = ""; + for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) { + if (signal?.aborted) throw new Error("aborted:debrid"); + try { + const res = await fetch(`${DEBRID_LINK_API_BASE}/downloader/add`, { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded", + Authorization: `Bearer ${apiKey}` + }, + body: `url=${encodeURIComponent(link)}`, + signal: withTimeoutSignal(signal, API_TIMEOUT_MS) + }); + + const json = await res.json() as Record; + + if (!json.success) { + const errorCode = String(json.error || ""); + const errorDesc = String(json.error_description || json.error || "Unbekannter Debrid-Link-Fehler"); + + if (DEBRID_LINK_QUOTA_ERRORS.has(errorCode)) { + logger.warn(`Debrid-Link Quota erreicht${keyLabel}: ${errorCode} – ${errorDesc}`); + break; + } + + if (errorCode === "badToken" || errorCode === "expired_token") { + throw new Error(`Debrid-Link${keyLabel}: Ungueltiger oder abgelaufener API-Key`); + } + if (errorCode === "floodDetected") { + await sleep(retryDelay(attempt), signal); + continue; + } + + throw new Error(`Debrid-Link${keyLabel}: ${errorDesc}`); + } + + const value = json.value as Record | undefined; + if (!value) { + throw new Error(`Debrid-Link${keyLabel}: Keine Daten in Antwort`); + } + + const directUrl = String(value.downloadUrl || ""); + if (!directUrl) { + throw new Error(`Debrid-Link${keyLabel}: Keine Download-URL in Antwort`); + } + + const fileName = String(value.name || "") || filenameFromUrl(directUrl) || filenameFromUrl(link); + const fileSize = typeof value.size === "number" && value.size > 0 ? value.size : null; + + return { + fileName, + directUrl, + fileSize, + retriesUsed: attempt - 1, + sourceLabel: `API${keyLabel}` + }; + } catch (error) { + lastError = compactErrorText(error); + if (signal?.aborted || (/aborted/i.test(lastError) && !/timeout/i.test(lastError))) { + throw error; + } + if (/Ungueltig|abgelaufen/i.test(lastError)) { + throw error; + } + if (attempt < REQUEST_RETRIES) { + await sleep(retryDelay(attempt), signal); + } + } + } + + this.currentKeyIndex = (this.currentKeyIndex + 1) % this.apiKeys.length; + if (this.currentKeyIndex === startIndex) { + triedAll = true; + } + } + + throw new Error(`Debrid-Link: Alle ${this.apiKeys.length} API-Keys haben ihr Limit erreicht`); + } +} + // ── 1Fichier Client ── class OneFichierClient { @@ -1509,6 +1618,8 @@ export class DebridService { private cachedDdownloadClient: DdownloadClient | null = null; private cachedDdownloadKey = ""; + private cachedDebridLinkClient: DebridLinkClient | null = null; + private cachedDebridLinkKey = ""; public constructor(settings: AppSettings, options: DebridServiceOptions = {}) { this.settings = cloneSettings(settings); @@ -1519,6 +1630,15 @@ export class DebridService { this.settings = cloneSettings(next); } + private getDebridLinkClient(apiKeysRaw: string): DebridLinkClient { + if (this.cachedDebridLinkClient && this.cachedDebridLinkKey === apiKeysRaw) { + return this.cachedDebridLinkClient; + } + this.cachedDebridLinkClient = new DebridLinkClient(apiKeysRaw); + this.cachedDebridLinkKey = apiKeysRaw; + return this.cachedDebridLinkClient; + } + private getDdownloadClient(login: string, password: string): DdownloadClient { const key = `${login}\0${password}`; if (this.cachedDdownloadClient && this.cachedDdownloadKey === key) { @@ -1724,6 +1844,9 @@ export class DebridService { if (provider === "onefichier") { return Boolean(settings.oneFichierApiKey.trim()); } + if (provider === "debridlink") { + return Boolean(settings.debridLinkApiKeys.trim()); + } return Boolean(this.shouldUseBestDebridWeb(settings) || settings.bestToken.trim()); } @@ -1763,6 +1886,11 @@ export class DebridService { if (provider === "onefichier") { return new OneFichierClient(settings.oneFichierApiKey).unrestrictLink(link, signal); } + if (provider === "debridlink") { + const dlResult = await this.getDebridLinkClient(settings.debridLinkApiKeys).unrestrictLink(link, signal); + dlResult.sourceLabel = dlResult.sourceLabel || "API"; + return dlResult; + } if (this.shouldUseBestDebridWeb(settings) && this.options.bestDebridWebUnrestrict) { const bdResult = await this.options.bestDebridWebUnrestrict(link, signal); if (!bdResult) { diff --git a/src/main/storage.ts b/src/main/storage.ts index bafe6ef..1d7593f 100644 --- a/src/main/storage.ts +++ b/src/main/storage.ts @@ -5,8 +5,8 @@ import { AppSettings, BandwidthScheduleEntry, DebridProvider, DownloadItem, Down import { defaultSettings } from "./constants"; import { logger } from "./logger"; -const VALID_PRIMARY_PROVIDERS = new Set(["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier"]); -const VALID_FALLBACK_PROVIDERS = new Set(["none", "realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier"]); +const VALID_PRIMARY_PROVIDERS = new Set(["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier", "debridlink"]); +const VALID_FALLBACK_PROVIDERS = new Set(["none", "realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier", "debridlink"]); const VALID_CLEANUP_MODES = new Set(["none", "trash", "delete"]); const VALID_CONFLICT_MODES = new Set(["overwrite", "skip", "rename", "ask"]); const VALID_FINISHED_POLICIES = new Set(["never", "immediate", "on_start", "package_done"]); @@ -17,7 +17,7 @@ const VALID_PACKAGE_PRIORITIES = new Set(["high", "normal", "low"]); const VALID_DOWNLOAD_STATUSES = new Set([ "queued", "validating", "downloading", "paused", "reconnect_wait", "extracting", "integrity_check", "completed", "failed", "cancelled" ]); -const VALID_ITEM_PROVIDERS = new Set(["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier"]); +const VALID_ITEM_PROVIDERS = new Set(["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier", "debridlink"]); const VALID_ONLINE_STATUSES = new Set(["online", "offline", "checking"]); function asText(value: unknown): string { @@ -118,6 +118,7 @@ export function normalizeSettings(settings: AppSettings): AppSettings { ddownloadLogin: asText(settings.ddownloadLogin), ddownloadPassword: asText(settings.ddownloadPassword), oneFichierApiKey: asText(settings.oneFichierApiKey), + debridLinkApiKeys: String(settings.debridLinkApiKeys ?? "").replace(/\r\n|\r/g, "\n").trim(), archivePasswordList: String(settings.archivePasswordList ?? "").replace(/\r\n|\r/g, "\n"), rememberToken: Boolean(settings.rememberToken), providerPrimary: settings.providerPrimary, @@ -212,7 +213,8 @@ function sanitizeCredentialPersistence(settings: AppSettings): AppSettings { allDebridToken: "", ddownloadLogin: "", ddownloadPassword: "", - oneFichierApiKey: "" + oneFichierApiKey: "", + debridLinkApiKeys: "" }; } diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index dd544b2..08ad9be 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -53,7 +53,7 @@ interface LinkPopupState { isPackage: boolean; } -type AccountService = "realdebrid" | "megadebrid" | "bestdebrid" | "alldebrid" | "ddownload" | "onefichier"; +type AccountService = "realdebrid" | "megadebrid" | "bestdebrid" | "alldebrid" | "ddownload" | "onefichier" | "debridlink"; type AccountKind = | "realdebrid-api" | "realdebrid-web" @@ -64,7 +64,8 @@ type AccountKind = | "alldebrid-api" | "alldebrid-web" | "ddownload-login" - | "onefichier-api"; + | "onefichier-api" + | "debridlink-api"; type AccountQuickAction = "realdebrid-login" | "bestdebrid-cookies" | "alldebrid-login" | "alldebrid-status"; type AccountColumnKey = "service" | "mode" | "status" | "secret"; @@ -185,10 +186,19 @@ const ACCOUNT_OPTIONS: AccountOption[] = [ modeLabel: "API", pickerDescription: "API-Key fuer 1fichier.com.", needsToken: true + }, + { + kind: "debridlink-api", + service: "debridlink", + serviceLabel: "Debrid-Link", + title: "Debrid-Link API", + modeLabel: "API", + pickerDescription: "API-Key(s) fuer debrid-link.com. Mehrere Keys zeilenweise fuer Multi-Account.", + needsToken: true } ]; -const ACCOUNT_SERVICES: AccountService[] = ["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier"]; +const ACCOUNT_SERVICES: AccountService[] = ["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier", "debridlink"]; const ACCOUNT_COLUMN_STORAGE_KEY = "rd-account-column-widths"; const ACCOUNT_COLUMN_DEFAULT_WIDTHS: Record = { service: 220, @@ -270,6 +280,9 @@ function getConfiguredProvidersFromSettings(settings: AppSettings): DebridProvid if (settings.allDebridUseWebLogin || settings.allDebridToken.trim()) { list.push("alldebrid"); } + if ((settings.debridLinkApiKeys || "").trim()) { + list.push("debridlink"); + } return list; } @@ -311,6 +324,8 @@ function getConfiguredAccountKind(settings: AppSettings, service: AccountService return settings.ddownloadLogin.trim() && settings.ddownloadPassword.trim() ? "ddownload-login" : null; case "onefichier": return settings.oneFichierApiKey.trim() ? "onefichier-api" : null; + case "debridlink": + return (settings.debridLinkApiKeys || "").trim() ? "debridlink-api" : null; default: return null; } @@ -346,6 +361,11 @@ function summarizeAccount(kind: AccountKind, settings: AppSettings): string { return settings.ddownloadLogin.trim() ? maskValue(settings.ddownloadLogin.trim(), 2, 6) : "Login + Passwort"; case "onefichier-api": return maskValue(settings.oneFichierApiKey, 3, 3); + case "debridlink-api": { + const keys = (settings.debridLinkApiKeys || "").split(/[\n,]+/).filter((k: string) => k.trim()); + if (keys.length > 1) return `${keys.length} API-Keys`; + return keys.length === 1 ? maskValue(keys[0].trim(), 3, 3) : "Nicht hinterlegt"; + } default: return "Konfiguriert"; } @@ -381,6 +401,8 @@ function createAccountDialogState(mode: "create" | "edit", kind: AccountKind | n return { mode, kind, token: "", login: settings.ddownloadLogin, password: settings.ddownloadPassword }; case "onefichier-api": return { mode, kind, token: settings.oneFichierApiKey, login: "", password: "" }; + case "debridlink-api": + return { mode, kind, token: settings.debridLinkApiKeys || "", login: "", password: "" }; default: return { mode, kind, token: "", login: "", password: "" }; } @@ -414,6 +436,8 @@ function applyAccountDialogToSettings(settings: AppSettings, dialog: AccountDial return { ...settings, ddownloadLogin: login, ddownloadPassword: password }; case "onefichier-api": return { ...settings, oneFichierApiKey: token }; + case "debridlink-api": + return { ...settings, debridLinkApiKeys: token }; default: return settings; } @@ -433,6 +457,8 @@ function clearAccountServiceFromSettings(settings: AppSettings, service: Account return { ...settings, ddownloadLogin: "", ddownloadPassword: "" }; case "onefichier": return { ...settings, oneFichierApiKey: "" }; + case "debridlink": + return { ...settings, debridLinkApiKeys: "" }; default: return settings; } @@ -499,7 +525,7 @@ const cleanupLabels: Record = { const AUTO_RENDER_PACKAGE_LIMIT = 260; const providerLabels: Record = { - realdebrid: "Real-Debrid", megadebrid: "Mega-Debrid", bestdebrid: "BestDebrid", alldebrid: "AllDebrid", ddownload: "DDownload", onefichier: "1Fichier" + realdebrid: "Real-Debrid", megadebrid: "Mega-Debrid", bestdebrid: "BestDebrid", alldebrid: "AllDebrid", ddownload: "DDownload", onefichier: "1Fichier", debridlink: "Debrid-Link" }; function formatDateTime(ts: number): string { diff --git a/src/shared/types.ts b/src/shared/types.ts index 7ab31ed..996a248 100644 --- a/src/shared/types.ts +++ b/src/shared/types.ts @@ -14,7 +14,7 @@ export type CleanupMode = "none" | "trash" | "delete"; export type ConflictMode = "overwrite" | "skip" | "rename" | "ask"; export type SpeedMode = "global" | "per_download"; export type FinishedCleanupPolicy = "never" | "immediate" | "on_start" | "package_done"; -export type DebridProvider = "realdebrid" | "megadebrid" | "bestdebrid" | "alldebrid" | "ddownload" | "onefichier"; +export type DebridProvider = "realdebrid" | "megadebrid" | "bestdebrid" | "alldebrid" | "ddownload" | "onefichier" | "debridlink"; export type DebridFallbackProvider = DebridProvider | "none"; export type AppTheme = "dark" | "light"; export type PackagePriority = "high" | "normal" | "low"; @@ -49,6 +49,7 @@ export interface AppSettings { ddownloadLogin: string; ddownloadPassword: string; oneFichierApiKey: string; + debridLinkApiKeys: string; archivePasswordList: string; rememberToken: boolean; providerPrimary: DebridProvider;