import type { AccountCheckScope, AppSettings, DebridAccountStatus, DebridProvider } from "../shared/types"; import { getMegaDebridAccountsForMode, getMegaDebridDisabledAccountIdsForMode, parseMegaDebridAccounts, type MegaDebridAccountEntry } from "../shared/mega-debrid-accounts"; import { parseDebridLinkApiKeys, type DebridLinkApiKeyEntry } from "../shared/debrid-link-keys"; import { getRealDebridAccounts, type RealDebridAccountEntry } from "../shared/real-debrid-accounts"; import { logger } from "./logger"; import { compactErrorText } from "./utils"; const MEGA_DEBRID_API = "https://www.mega-debrid.eu/api.php"; const DEBRID_LINK_API = "https://debrid-link.com/api/v2"; const REAL_DEBRID_USER_API = "https://api.real-debrid.com/rest/1.0/user"; const CHECK_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36"; const CHECK_TIMEOUT_MS = 20000; export const REAL_DEBRID_STATUS_ID = "svc-realdebrid"; export interface RealDebridSessionProbeResult { valid: boolean; isPremium?: boolean; premiumUntilMs?: number | null; username?: string; email?: string; message?: string; } export type RealDebridSessionProbe = (signal?: AbortSignal) => Promise; export type RealDebridSessionProbeResolver = (accountId: string, signal?: AbortSignal) => Promise; export function retainConfiguredRealDebridStatuses(settings: AppSettings, statuses: readonly DebridAccountStatus[]): DebridAccountStatus[] { const accountIds = new Set(getRealDebridAccounts(settings).map((account) => account.id)); if (accountIds.size === 0 && (settings.realDebridUseWebLogin || settings.token.trim())) { accountIds.add(REAL_DEBRID_STATUS_ID); } return statuses.filter((status) => status.provider !== "realdebrid" || accountIds.has(status.accountId)); } function timeoutSignal(signal: AbortSignal | undefined, ms: number): AbortSignal { const timeout = AbortSignal.timeout(ms); return signal ? AbortSignal.any([signal, timeout]) : timeout; } function parseJsonSafe(text: string): Record | null { try { const parsed = JSON.parse(text) as unknown; return parsed && typeof parsed === "object" ? (parsed as Record) : null; } catch { return null; } } function formatRemaining(premiumUntilMs: number | null, now: number): string { if (premiumUntilMs == null) { return "Premium-Status unbekannt"; } if (premiumUntilMs <= 0) { return "Kein Premium"; } const remainingMs = premiumUntilMs - now; if (remainingMs <= 0) { return "Premium abgelaufen"; } const days = Math.floor(remainingMs / (24 * 60 * 60 * 1000)); if (days >= 1) { return `Premium noch ${days} Tag${days === 1 ? "" : "e"}`; } const hours = Math.max(1, Math.floor(remainingMs / (60 * 60 * 1000))); return `Premium noch ${hours} Std`; } function maskSecret(value: string): string { const trimmed = value.trim(); if (!trimmed) { return ""; } if (trimmed.length <= 6) { return "*".repeat(trimmed.length); } return `${trimmed.slice(0, 3)}${"*".repeat(Math.max(4, trimmed.length - 6))}${trimmed.slice(-3)}`; } export async function checkRealDebridAccount( accountOrSettings: RealDebridAccountEntry | AppSettings, signal?: AbortSignal, now = Date.now(), probeWebSession?: RealDebridSessionProbe ): Promise { const isAccount = typeof (accountOrSettings as RealDebridAccountEntry).kind === "string"; const account = isAccount ? accountOrSettings as RealDebridAccountEntry : { id: REAL_DEBRID_STATUS_ID, kind: (accountOrSettings as AppSettings).realDebridUseWebLogin ? "web" as const : "api" as const, index: 0, label: "Real-Debrid", maskedLogin: (accountOrSettings as AppSettings).realDebridUseWebLogin ? "Browser-Login" : maskSecret(String((accountOrSettings as AppSettings).token || "")), enabled: true, ...((accountOrSettings as AppSettings).realDebridUseWebLogin ? {} : { token: String((accountOrSettings as AppSettings).token || "").trim() }) } as RealDebridAccountEntry; const token = account.kind === "api" ? account.token.trim() : ""; const useWebLogin = account.kind === "web"; const base: DebridAccountStatus = { accountId: account.id, provider: "realdebrid", label: account.label, maskedLogin: account.maskedLogin, valid: false, isPremium: false, premiumUntilMs: null, message: "", checkedAt: now }; if (useWebLogin) { if (!probeWebSession) { return { ...base, message: "Browser-Sitzung ist nicht prüfbar" }; } try { const result = await probeWebSession(signal); if (!result.valid) { return { ...base, message: result.message || "Browser-Sitzung abgelaufen" }; } const premiumUntilMs = typeof result.premiumUntilMs === "number" ? result.premiumUntilMs : null; return { ...base, valid: true, isPremium: Boolean(result.isPremium), premiumUntilMs, username: String(result.username || "").trim() || undefined, email: String(result.email || "").trim() || undefined, message: result.message || (result.isPremium ? formatRemaining(premiumUntilMs, now) : "Kein Premium (Free)") }; } catch (error) { const errText = compactErrorText(error); const aborted = signal?.aborted || /aborted/i.test(errText); return { ...base, message: aborted ? "Prüfung abgebrochen" : `Prüfung fehlgeschlagen: ${errText}` }; } } if (!token) { return { ...base, message: "Kein API-Token hinterlegt" }; } try { const response = await fetch(REAL_DEBRID_USER_API, { headers: { Authorization: `Bearer ${token}`, "User-Agent": CHECK_USER_AGENT }, signal: timeoutSignal(signal, CHECK_TIMEOUT_MS) }); const text = await response.text(); const payload = parseJsonSafe(text); if (!response.ok || !payload) { if (response.status === 401 || response.status === 403) { return { ...base, message: "Ungültiger API-Token" }; } return { ...base, message: `Prüfung fehlgeschlagen (HTTP ${response.status})` }; } const expiration = Date.parse(String(payload.expiration || "")); const premiumUntilMs = Number.isFinite(expiration) ? expiration : null; const isPremium = String(payload.type || "").toLowerCase() === "premium" && (premiumUntilMs == null || premiumUntilMs > now); return { ...base, valid: true, isPremium, premiumUntilMs, username: String(payload.username || "").trim() || undefined, email: String(payload.email || "").trim() || undefined, message: isPremium ? formatRemaining(premiumUntilMs, now) : "Kein Premium (Free)" }; } catch (error) { const errText = compactErrorText(error); const aborted = signal?.aborted || /aborted/i.test(errText); return { ...base, message: aborted ? "Prüfung abgebrochen" : `Prüfung fehlgeschlagen: ${errText}` }; } } export async function checkMegaDebridAccount( account: MegaDebridAccountEntry, signal?: AbortSignal, now = Date.now() ): Promise { const base: DebridAccountStatus = { accountId: account.id, provider: "megadebrid", label: account.label, maskedLogin: account.maskedLogin, valid: false, isPremium: false, premiumUntilMs: null, message: "", checkedAt: now }; try { const url = `${MEGA_DEBRID_API}?action=connectUser&login=${encodeURIComponent(account.login)}&password=${encodeURIComponent(account.password)}`; const response = await fetch(url, { headers: { "User-Agent": CHECK_USER_AGENT }, signal: timeoutSignal(signal, CHECK_TIMEOUT_MS) }); const text = await response.text(); const payload = parseJsonSafe(text); if (!response.ok || !payload) { return { ...base, message: `Login fehlgeschlagen (HTTP ${response.status})` }; } if (payload.response_code !== "ok") { const reason = String(payload.response_text || payload.response_code || "Login abgelehnt"); return { ...base, message: `Ungueltiger Login: ${reason}` }; } const vipEndRaw = Number(payload.vip_end || 0); const premiumUntilMs = Number.isFinite(vipEndRaw) && vipEndRaw > 0 ? vipEndRaw * 1000 : 0; const isPremium = premiumUntilMs > now; const email = String(payload.email || "").trim() || undefined; return { ...base, valid: true, isPremium, premiumUntilMs, email, message: formatRemaining(premiumUntilMs, now) }; } catch (error) { const errText = compactErrorText(error); const aborted = signal?.aborted || /aborted/i.test(errText); return { ...base, message: aborted ? "Pruefung abgebrochen" : `Pruefung fehlgeschlagen: ${errText}` }; } } export async function checkDebridLinkKey( key: DebridLinkApiKeyEntry, signal?: AbortSignal, now = Date.now() ): Promise { const base: DebridAccountStatus = { accountId: key.id, provider: "debridlink", label: key.label, maskedLogin: key.masked, valid: false, isPremium: false, premiumUntilMs: null, message: "", checkedAt: now }; try { const response = await fetch(`${DEBRID_LINK_API}/account/infos`, { headers: { Authorization: `Bearer ${key.token}`, "User-Agent": CHECK_USER_AGENT }, signal: timeoutSignal(signal, CHECK_TIMEOUT_MS) }); const text = await response.text(); const payload = parseJsonSafe(text); if (!response.ok || !payload) { if (response.status === 401 || response.status === 403) { return { ...base, message: "Ungueltiger API-Key (nicht autorisiert)" }; } return { ...base, message: `Pruefung fehlgeschlagen (HTTP ${response.status})` }; } if (payload.success === false) { const reason = String(payload.error || "Key abgelehnt"); return { ...base, message: `Ungueltiger API-Key: ${reason}` }; } const value = (payload.value && typeof payload.value === "object" ? payload.value : payload) as Record; const premiumLeftSec = Number(value.premiumLeft || 0); const accountType = Number(value.accountType || 0); const premiumUntilMs = Number.isFinite(premiumLeftSec) && premiumLeftSec > 0 ? now + premiumLeftSec * 1000 : 0; const isPremium = premiumUntilMs > now || accountType > 0; const username = String(value.username || "").trim() || undefined; const email = String(value.email || "").trim() || undefined; return { ...base, valid: true, isPremium, premiumUntilMs: premiumUntilMs > 0 ? premiumUntilMs : (accountType > 0 ? null : 0), username, email, message: premiumUntilMs > 0 ? formatRemaining(premiumUntilMs, now) : (accountType > 0 ? "Premium aktiv" : "Kein Premium (Free)") }; } catch (error) { const errText = compactErrorText(error); const aborted = signal?.aborted || /aborted/i.test(errText); return { ...base, message: aborted ? "Pruefung abgebrochen" : `Pruefung fehlgeschlagen: ${errText}` }; } } export async function checkAllDebridAccounts( settings: AppSettings, signal?: AbortSignal, probeRealDebridWebSession?: RealDebridSessionProbeResolver, scope: AccountCheckScope = "all" ): Promise { const now = Date.now(); const providerEnabled = (provider: DebridProvider): boolean => !(settings.disabledProviders || []).includes(provider); const configuredMegaAccounts = parseMegaDebridAccounts(settings.megaCredentials || "", settings.megaPassword || ""); const activeMegaAccounts = (["api", "web"] as const).flatMap((mode) => { const provider: DebridProvider = mode === "api" ? "megadebrid-api" : "megadebrid-web"; const modeEnabled = mode === "api" ? settings.megaDebridApiEnabled : settings.megaDebridWebEnabled; if (!modeEnabled || !providerEnabled(provider) || !providerEnabled("megadebrid")) { return []; } const disabledIds = new Set(getMegaDebridDisabledAccountIdsForMode(settings, mode)); return getMegaDebridAccountsForMode(settings, mode).filter((account) => !disabledIds.has(account.id)); }); const megaAccounts = scope === "all" ? configuredMegaAccounts : [...new Map(activeMegaAccounts.map((account) => [account.id, account])).values()]; const configuredDebridLinkKeys = parseDebridLinkApiKeys(settings.debridLinkApiKeys || ""); const debridLinkKeys = scope === "all" ? configuredDebridLinkKeys : providerEnabled("debridlink") ? configuredDebridLinkKeys.filter((key) => !(settings.debridLinkDisabledKeyIds || []).includes(key.id)) : []; const configuredRealDebridAccounts = getRealDebridAccounts(settings); const legacyRealDebridAccounts: RealDebridAccountEntry[] = configuredRealDebridAccounts.length === 0 && (settings.realDebridUseWebLogin || String(settings.token || "").trim()) ? [{ id: REAL_DEBRID_STATUS_ID, kind: settings.realDebridUseWebLogin ? "web" : "api", index: 0, label: "Real-Debrid", maskedLogin: settings.realDebridUseWebLogin ? "Browser-Login" : maskSecret(settings.token), enabled: true, ...(settings.realDebridUseWebLogin ? {} : { token: settings.token.trim() }) } as RealDebridAccountEntry] : []; const allRealDebridAccounts = configuredRealDebridAccounts.length > 0 ? configuredRealDebridAccounts : legacyRealDebridAccounts; const realDebridAccounts = scope === "all" ? allRealDebridAccounts : providerEnabled("realdebrid") ? allRealDebridAccounts.filter((account) => account.enabled) : []; const taskFns: Array<() => Promise> = [ ...realDebridAccounts.map((account) => () => checkRealDebridAccount( account, signal, now, account.kind === "web" && probeRealDebridWebSession ? (probeSignal) => probeRealDebridWebSession(account.id, probeSignal) : undefined )), ...megaAccounts.map((account) => () => checkMegaDebridAccount(account, signal, now)), ...debridLinkKeys.map((key) => () => checkDebridLinkKey(key, signal, now)) ]; const results = await runWithConcurrency(taskFns, CHECK_CONCURRENCY); logger.info( `Account-Check abgeschlossen: scope=${scope}, ${results.length} Accounts geprueft ` + `(${results.filter((r) => r.valid).length} gueltig, ${results.filter((r) => r.isPremium).length} premium)` ); return results; } const CHECK_CONCURRENCY = 4; async function runWithConcurrency(taskFns: Array<() => Promise>, limit: number): Promise { const results: T[] = new Array(taskFns.length); let nextIndex = 0; const worker = async (): Promise => { while (nextIndex < taskFns.length) { const current = nextIndex; nextIndex += 1; results[current] = await taskFns[current](); } }; const workers = Array.from({ length: Math.min(limit, taskFns.length) }, () => worker()); await Promise.all(workers); return results; }