From 22ed37d67c78e6e8ae7d291f40e4c5547d9c6e28 Mon Sep 17 00:00:00 2001 From: Sucukdeluxe Date: Fri, 6 Mar 2026 19:00:19 +0100 Subject: [PATCH] feat: add LinkSnappy provider, account deactivation, UI polish - Add LinkSnappy provider with cookie-based session auth and /api/linkgen - Upgrade LinkSnappy download URLs from http to https (fix 425 errors) - Add account deactivation toggle (disabledProviders in settings) - Show account type (API/Web/Login) in provider dropdowns - Show API key count for Debrid-Link in status label - Fix all missing German umlauts throughout the UI - Wider modal for textarea, compact action buttons in one row - Debrid-Link: log which API key (#1/#2) is used for unrestrict Co-Authored-By: Claude Opus 4.6 --- src/main/constants.ts | 5 +- src/main/debrid.ts | 178 ++++++++++++++++++++++++++++++++++++++-- src/main/storage.ts | 13 ++- src/renderer/App.tsx | 127 ++++++++++++++++++++-------- src/renderer/styles.css | 27 ++++-- src/shared/types.ts | 5 +- 6 files changed, 301 insertions(+), 54 deletions(-) diff --git a/src/main/constants.ts b/src/main/constants.ts index 55ba356..b956533 100644 --- a/src/main/constants.ts +++ b/src/main/constants.ts @@ -53,6 +53,8 @@ export function defaultSettings(): AppSettings { ddownloadPassword: "", oneFichierApiKey: "", debridLinkApiKeys: "", + linkSnappyLogin: "", + linkSnappyPassword: "", archivePasswordList: "", rememberToken: true, providerPrimary: "realdebrid", @@ -95,6 +97,7 @@ export function defaultSettings(): AppSettings { bandwidthSchedules: [], columnOrder: ["name", "size", "progress", "hoster", "account", "prio", "status", "speed"], extractCpuPriority: "high", - autoExtractWhenStopped: true + autoExtractWhenStopped: true, + disabledProviders: [] }; } diff --git a/src/main/debrid.ts b/src/main/debrid.ts index 1820ae4..fe0a7bb 100644 --- a/src/main/debrid.ts +++ b/src/main/debrid.ts @@ -20,6 +20,8 @@ const ONEFICHIER_URL_RE = /^https?:\/\/(?:www\.)?(?:1fichier\.com|alterupload\.c 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 LINKSNAPPY_API_BASE = "https://linksnappy.com/api"; + const PROVIDER_LABELS: Record = { realdebrid: "Real-Debrid", megadebrid: "Mega-Debrid", @@ -27,7 +29,8 @@ const PROVIDER_LABELS: Record = { alldebrid: "AllDebrid", ddownload: "DDownload", onefichier: "1Fichier", - debridlink: "Debrid-Link" + debridlink: "Debrid-Link", + linksnappy: "LinkSnappy" }; interface ProviderUnrestrictedLink extends UnrestrictedLink { @@ -1279,7 +1282,7 @@ class DebridLinkClient { while (!triedAll) { const apiKey = this.apiKeys[this.currentKeyIndex]; - const keyLabel = this.apiKeys.length > 1 ? ` (Key ${this.currentKeyIndex + 1}/${this.apiKeys.length})` : ""; + const keyLabel = this.apiKeys.length > 1 ? ` #${this.currentKeyIndex + 1}` : ""; let lastError = ""; for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) { @@ -1307,7 +1310,7 @@ class DebridLinkClient { } if (errorCode === "badToken" || errorCode === "expired_token") { - throw new Error(`Debrid-Link${keyLabel}: Ungueltiger oder abgelaufener API-Key`); + throw new Error(`Debrid-Link${keyLabel}: Ungültiger oder abgelaufener API-Key`); } if (errorCode === "floodDetected") { await sleep(retryDelay(attempt), signal); @@ -1330,19 +1333,21 @@ class DebridLinkClient { const fileName = String(value.name || "") || filenameFromUrl(directUrl) || filenameFromUrl(link); const fileSize = typeof value.size === "number" && value.size > 0 ? value.size : null; + logger.info(`Debrid-Link${keyLabel}: Unrestrict OK → ${fileName || "?"}`); + return { fileName, directUrl, fileSize, retriesUsed: attempt - 1, - sourceLabel: `API${keyLabel}` + sourceLabel: keyLabel ? `#${this.currentKeyIndex + 1}` : "API" }; } catch (error) { lastError = compactErrorText(error); if (signal?.aborted || (/aborted/i.test(lastError) && !/timeout/i.test(lastError))) { throw error; } - if (/Ungueltig|abgelaufen/i.test(lastError)) { + if (/Ungültig|abgelaufen/i.test(lastError)) { throw error; } if (attempt < REQUEST_RETRIES) { @@ -1361,6 +1366,150 @@ class DebridLinkClient { } } +// ── LinkSnappy Client ── + +class LinkSnappyClient { + private username: string; + private password: string; + private sessionCookies: string | null = null; + + public constructor(username: string, password: string) { + this.username = username; + this.password = password; + } + + private async authenticate(signal?: AbortSignal): Promise { + const params = new URLSearchParams({ username: this.username, password: this.password }); + const res = await fetch(`${LINKSNAPPY_API_BASE}/AUTHENTICATE?${params.toString()}`, { + signal: withTimeoutSignal(signal, API_TIMEOUT_MS), + redirect: "manual" + }); + + const cookies: string[] = []; + const setCookie = res.headers.getSetCookie?.() ?? []; + for (const sc of setCookie) { + const nameValue = sc.split(";")[0]; + if (nameValue) cookies.push(nameValue); + } + + const json = await res.json() as Record; + if (json.status !== "OK") { + throw new Error(`LinkSnappy: Login fehlgeschlagen – ${String(json.error || "Unbekannter Fehler")}`); + } + + if (cookies.length > 0) { + this.sessionCookies = cookies.join("; "); + } else { + this.sessionCookies = `username=${encodeURIComponent(this.username)}; Auth=manual`; + } + + logger.info("LinkSnappy: Authentifizierung erfolgreich"); + } + + public async unrestrictLink(link: string, signal?: AbortSignal): Promise { + if (!this.username || !this.password) { + throw new Error("LinkSnappy: Kein Login konfiguriert"); + } + + let lastError = ""; + for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) { + if (signal?.aborted) throw new Error("aborted:debrid"); + try { + if (!this.sessionCookies) { + await this.authenticate(signal); + } + + const genLinks = `{"link":"${encodeURIComponent(link)}","type":"","linkpass":""}`; + const url = `${LINKSNAPPY_API_BASE}/linkgen?genLinks=${genLinks}`; + + const res = await fetch(url, { + headers: { Cookie: this.sessionCookies! }, + signal: withTimeoutSignal(signal, API_TIMEOUT_MS) + }); + + const json = await res.json() as Record; + + if (json.status === "ERROR" && json.error) { + const errorMsg = String(json.error); + if (/not logged in|session expired|unauthorized/i.test(errorMsg)) { + this.sessionCookies = null; + if (attempt < REQUEST_RETRIES) { + continue; + } + throw new Error(`LinkSnappy: ${errorMsg}`); + } + throw new Error(`LinkSnappy: ${errorMsg}`); + } + + const links = json.links as Array> | undefined; + if (!links || links.length === 0) { + throw new Error("LinkSnappy: Keine Antwort-Daten"); + } + + const entry = links[0]; + if (entry.status === "ERROR" || (entry.error && entry.status !== "OK")) { + const errText = String(entry.error); + if (/quota|limit/i.test(errText)) { + throw new Error(`LinkSnappy: Quota erreicht – ${errText}`); + } + throw new Error(`LinkSnappy: ${errText}`); + } + + let directUrl = String(entry.generated || ""); + if (!directUrl) { + throw new Error("LinkSnappy: Keine Download-URL in Antwort"); + } + // LinkSnappy liefert http:// URLs – auf https:// upgraden (deren Server unterstützt beides) + if (directUrl.startsWith("http://")) { + directUrl = directUrl.replace("http://", "https://"); + } + + const fileName = String(entry.filename || "") || filenameFromUrl(directUrl) || filenameFromUrl(link); + const rawSize = entry.filesize; + let fileSize: number | null = null; + if (typeof rawSize === "number" && rawSize > 0) { + fileSize = rawSize; + } else if (typeof rawSize === "string") { + const parsed = parseFileSizeString(rawSize); + if (parsed > 0) fileSize = parsed; + } + + logger.info(`LinkSnappy: Unrestrict OK → ${fileName || "?"}`); + + return { + fileName, + directUrl, + fileSize, + retriesUsed: attempt - 1, + sourceLabel: "API" + }; + } catch (error) { + lastError = compactErrorText(error); + if (signal?.aborted || (/aborted/i.test(lastError) && !/timeout/i.test(lastError))) { + throw error; + } + if (/fehlgeschlagen/i.test(lastError) && /Login/i.test(lastError)) { + throw error; + } + if (attempt < REQUEST_RETRIES) { + await sleep(retryDelay(attempt), signal); + } + } + } + + throw new Error(String(lastError || "LinkSnappy Unrestrict fehlgeschlagen").replace(/^Error:\s*/i, "")); + } +} + +function parseFileSizeString(s: string): number { + const match = s.trim().match(/^([\d.]+)\s*([KMGT]?)B?$/i); + if (!match) return 0; + const num = parseFloat(match[1]); + const unit = (match[2] || "").toUpperCase(); + const multipliers: Record = { "": 1, K: 1024, M: 1024 ** 2, G: 1024 ** 3, T: 1024 ** 4 }; + return Math.floor(num * (multipliers[unit] || 1)); +} + // ── 1Fichier Client ── class OneFichierClient { @@ -1620,6 +1769,8 @@ export class DebridService { private cachedDdownloadKey = ""; private cachedDebridLinkClient: DebridLinkClient | null = null; private cachedDebridLinkKey = ""; + private cachedLinkSnappyClient: LinkSnappyClient | null = null; + private cachedLinkSnappyKey = ""; public constructor(settings: AppSettings, options: DebridServiceOptions = {}) { this.settings = cloneSettings(settings); @@ -1639,6 +1790,16 @@ export class DebridService { return this.cachedDebridLinkClient; } + private getLinkSnappyClient(login: string, password: string): LinkSnappyClient { + const key = `${login}\0${password}`; + if (this.cachedLinkSnappyClient && this.cachedLinkSnappyKey === key) { + return this.cachedLinkSnappyClient; + } + this.cachedLinkSnappyClient = new LinkSnappyClient(login, password); + this.cachedLinkSnappyKey = key; + return this.cachedLinkSnappyClient; + } + private getDdownloadClient(login: string, password: string): DdownloadClient { const key = `${login}\0${password}`; if (this.cachedDdownloadClient && this.cachedDdownloadKey === key) { @@ -1829,6 +1990,7 @@ export class DebridService { } private isProviderConfiguredFor(settings: AppSettings, provider: DebridProvider): boolean { + if ((settings.disabledProviders || []).includes(provider)) return false; if (provider === "realdebrid") { return Boolean(this.shouldUseRealDebridWeb(settings) || settings.token.trim()); } @@ -1847,6 +2009,9 @@ export class DebridService { if (provider === "debridlink") { return Boolean(settings.debridLinkApiKeys.trim()); } + if (provider === "linksnappy") { + return Boolean(settings.linkSnappyLogin.trim() && settings.linkSnappyPassword.trim()); + } return Boolean(this.shouldUseBestDebridWeb(settings) || settings.bestToken.trim()); } @@ -1891,6 +2056,9 @@ export class DebridService { dlResult.sourceLabel = dlResult.sourceLabel || "API"; return dlResult; } + if (provider === "linksnappy") { + return this.getLinkSnappyClient(settings.linkSnappyLogin, settings.linkSnappyPassword).unrestrictLink(link, signal); + } 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 1d7593f..8df22ee 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", "debridlink"]); -const VALID_FALLBACK_PROVIDERS = new Set(["none", "realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier", "debridlink"]); +const VALID_PRIMARY_PROVIDERS = new Set(["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier", "debridlink", "linksnappy"]); +const VALID_FALLBACK_PROVIDERS = new Set(["none", "realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier", "debridlink", "linksnappy"]); 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"]); @@ -119,6 +119,8 @@ export function normalizeSettings(settings: AppSettings): AppSettings { ddownloadPassword: asText(settings.ddownloadPassword), oneFichierApiKey: asText(settings.oneFichierApiKey), debridLinkApiKeys: String(settings.debridLinkApiKeys ?? "").replace(/\r\n|\r/g, "\n").trim(), + linkSnappyLogin: asText(settings.linkSnappyLogin), + linkSnappyPassword: asText(settings.linkSnappyPassword), archivePasswordList: String(settings.archivePasswordList ?? "").replace(/\r\n|\r/g, "\n"), rememberToken: Boolean(settings.rememberToken), providerPrimary: settings.providerPrimary, @@ -161,7 +163,8 @@ export function normalizeSettings(settings: AppSettings): AppSettings { bandwidthSchedules: normalizeBandwidthSchedules(settings.bandwidthSchedules), columnOrder: normalizeColumnOrder(settings.columnOrder), extractCpuPriority: settings.extractCpuPriority, - autoExtractWhenStopped: settings.autoExtractWhenStopped !== undefined ? Boolean(settings.autoExtractWhenStopped) : defaults.autoExtractWhenStopped + autoExtractWhenStopped: settings.autoExtractWhenStopped !== undefined ? Boolean(settings.autoExtractWhenStopped) : defaults.autoExtractWhenStopped, + disabledProviders: Array.isArray(settings.disabledProviders) ? settings.disabledProviders.filter((p: unknown) => VALID_PRIMARY_PROVIDERS.has(p as string)) as DebridProvider[] : [] }; if (!VALID_PRIMARY_PROVIDERS.has(normalized.providerPrimary)) { @@ -214,7 +217,9 @@ function sanitizeCredentialPersistence(settings: AppSettings): AppSettings { ddownloadLogin: "", ddownloadPassword: "", oneFichierApiKey: "", - debridLinkApiKeys: "" + debridLinkApiKeys: "", + linkSnappyLogin: "", + linkSnappyPassword: "" }; } diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index 08ad9be..aed70e5 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" | "debridlink"; +type AccountService = "realdebrid" | "megadebrid" | "bestdebrid" | "alldebrid" | "ddownload" | "onefichier" | "debridlink" | "linksnappy"; type AccountKind = | "realdebrid-api" | "realdebrid-web" @@ -65,7 +65,8 @@ type AccountKind = | "alldebrid-web" | "ddownload-login" | "onefichier-api" - | "debridlink-api"; + | "debridlink-api" + | "linksnappy-login"; type AccountQuickAction = "realdebrid-login" | "bestdebrid-cookies" | "alldebrid-login" | "alldebrid-status"; type AccountColumnKey = "service" | "mode" | "status" | "secret"; @@ -97,6 +98,7 @@ interface ConfiguredAccountEntry { statusLabel: string; summary: string; note: string; + disabled: boolean; } const ACCOUNT_OPTIONS: AccountOption[] = [ @@ -106,7 +108,7 @@ const ACCOUNT_OPTIONS: AccountOption[] = [ serviceLabel: "Real-Debrid", title: "Real-Debrid API", modeLabel: "API", - pickerDescription: "Direkter Zugriff ueber API-Token.", + pickerDescription: "Direkter Zugriff über API-Token.", needsToken: true }, { @@ -115,7 +117,7 @@ const ACCOUNT_OPTIONS: AccountOption[] = [ serviceLabel: "Real-Debrid", title: "Real-Debrid Web", modeLabel: "Web", - pickerDescription: "Login ueber Browserfenster statt Token." + pickerDescription: "Login über Browserfenster statt Token." }, { kind: "megadebrid-api", @@ -123,7 +125,7 @@ const ACCOUNT_OPTIONS: AccountOption[] = [ serviceLabel: "Mega-Debrid", title: "Mega-Debrid API", modeLabel: "API", - pickerDescription: "Login mit API-Praeferenz und Web-Fallback.", + pickerDescription: "Login mit API-Präferenz und Web-Fallback.", needsCredentials: true }, { @@ -132,7 +134,7 @@ const ACCOUNT_OPTIONS: AccountOption[] = [ serviceLabel: "Mega-Debrid", title: "Mega-Debrid Web", modeLabel: "Web", - pickerDescription: "Login mit Web-Praeferenz ueber Nutzername und Passwort.", + pickerDescription: "Login mit Web-Präferenz über Nutzername und Passwort.", needsCredentials: true }, { @@ -141,7 +143,7 @@ const ACCOUNT_OPTIONS: AccountOption[] = [ serviceLabel: "BestDebrid", title: "BestDebrid API", modeLabel: "API", - pickerDescription: "Direkter Zugriff ueber API-Token.", + pickerDescription: "Direkter Zugriff über API-Token.", needsToken: true }, { @@ -158,7 +160,7 @@ const ACCOUNT_OPTIONS: AccountOption[] = [ serviceLabel: "AllDebrid", title: "AllDebrid API", modeLabel: "API", - pickerDescription: "Direkter Zugriff ueber API-Key.", + pickerDescription: "Direkter Zugriff über API-Key.", needsToken: true }, { @@ -167,7 +169,7 @@ const ACCOUNT_OPTIONS: AccountOption[] = [ serviceLabel: "AllDebrid", title: "AllDebrid Web", modeLabel: "Web", - pickerDescription: "Login ueber Browserfenster fuer reCAPTCHA.", + pickerDescription: "Login über Browserfenster für reCAPTCHA.", }, { kind: "ddownload-login", @@ -175,7 +177,7 @@ const ACCOUNT_OPTIONS: AccountOption[] = [ serviceLabel: "DDownload", title: "DDownload Login", modeLabel: "Login", - pickerDescription: "Direkter Login fuer ddownload.com und ddl.to.", + pickerDescription: "Direkter Login für ddownload.com und ddl.to.", needsCredentials: true }, { @@ -184,7 +186,7 @@ const ACCOUNT_OPTIONS: AccountOption[] = [ serviceLabel: "1Fichier", title: "1Fichier API", modeLabel: "API", - pickerDescription: "API-Key fuer 1fichier.com.", + pickerDescription: "API-Key für 1fichier.com.", needsToken: true }, { @@ -193,12 +195,21 @@ const ACCOUNT_OPTIONS: AccountOption[] = [ serviceLabel: "Debrid-Link", title: "Debrid-Link API", modeLabel: "API", - pickerDescription: "API-Key(s) fuer debrid-link.com. Mehrere Keys zeilenweise fuer Multi-Account.", + pickerDescription: "API-Key(s) für debrid-link.com. Mehrere Keys zeilenweise für Multi-Account.", needsToken: true + }, + { + kind: "linksnappy-login", + service: "linksnappy", + serviceLabel: "LinkSnappy", + title: "LinkSnappy Login", + modeLabel: "Login", + pickerDescription: "Login für linksnappy.com mit Benutzername und Passwort.", + needsCredentials: true } ]; -const ACCOUNT_SERVICES: AccountService[] = ["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier", "debridlink"]; +const ACCOUNT_SERVICES: AccountService[] = ["realdebrid", "megadebrid", "bestdebrid", "alldebrid", "ddownload", "onefichier", "debridlink", "linksnappy"]; const ACCOUNT_COLUMN_STORAGE_KEY = "rd-account-column-widths"; const ACCOUNT_COLUMN_DEFAULT_WIDTHS: Record = { service: 220, @@ -283,11 +294,19 @@ function getConfiguredProvidersFromSettings(settings: AppSettings): DebridProvid if ((settings.debridLinkApiKeys || "").trim()) { list.push("debridlink"); } + if ((settings.linkSnappyLogin || "").trim() && (settings.linkSnappyPassword || "").trim()) { + list.push("linksnappy"); + } return list; } +function getActiveProvidersFromSettings(settings: AppSettings): DebridProvider[] { + const disabled = new Set(settings.disabledProviders || []); + return getConfiguredProvidersFromSettings(settings).filter((p) => !disabled.has(p)); +} + function normalizeProviderSelectionForSettings(settings: AppSettings): Pick { - const configuredProviders = getConfiguredProvidersFromSettings(settings); + const configuredProviders = getActiveProvidersFromSettings(settings); const primaryProvider = configuredProviders.includes(settings.providerPrimary) ? settings.providerPrimary : (configuredProviders[0] ?? "realdebrid"); @@ -326,6 +345,8 @@ function getConfiguredAccountKind(settings: AppSettings, service: AccountService return settings.oneFichierApiKey.trim() ? "onefichier-api" : null; case "debridlink": return (settings.debridLinkApiKeys || "").trim() ? "debridlink-api" : null; + case "linksnappy": + return (settings.linkSnappyLogin || "").trim() && (settings.linkSnappyPassword || "").trim() ? "linksnappy-login" : null; default: return null; } @@ -366,6 +387,8 @@ function summarizeAccount(kind: AccountKind, settings: AppSettings): string { if (keys.length > 1) return `${keys.length} API-Keys`; return keys.length === 1 ? maskValue(keys[0].trim(), 3, 3) : "Nicht hinterlegt"; } + case "linksnappy-login": + return (settings.linkSnappyLogin || "").trim() ? maskValue((settings.linkSnappyLogin || "").trim(), 2, 4) : "Login + Passwort"; default: return "Konfiguriert"; } @@ -403,6 +426,8 @@ function createAccountDialogState(mode: "create" | "edit", kind: AccountKind | n return { mode, kind, token: settings.oneFichierApiKey, login: "", password: "" }; case "debridlink-api": return { mode, kind, token: settings.debridLinkApiKeys || "", login: "", password: "" }; + case "linksnappy-login": + return { mode, kind, token: "", login: settings.linkSnappyLogin || "", password: settings.linkSnappyPassword || "" }; default: return { mode, kind, token: "", login: "", password: "" }; } @@ -438,6 +463,8 @@ function applyAccountDialogToSettings(settings: AppSettings, dialog: AccountDial return { ...settings, oneFichierApiKey: token }; case "debridlink-api": return { ...settings, debridLinkApiKeys: token }; + case "linksnappy-login": + return { ...settings, linkSnappyLogin: login, linkSnappyPassword: password }; default: return settings; } @@ -459,6 +486,8 @@ function clearAccountServiceFromSettings(settings: AppSettings, service: Account return { ...settings, oneFichierApiKey: "" }; case "debridlink": return { ...settings, debridLinkApiKeys: "" }; + case "linksnappy": + return { ...settings, linkSnappyLogin: "", linkSnappyPassword: "" }; default: return settings; } @@ -466,7 +495,7 @@ function clearAccountServiceFromSettings(settings: AppSettings, service: Account function validateAccountDialog(dialog: AccountDialogState): string | null { if (!dialog.kind) { - return "Bitte zuerst einen Account-Typ auswaehlen."; + return "Bitte zuerst einen Account-Typ auswählen."; } const option = findAccountOption(dialog.kind); if (option.needsToken && !dialog.token.trim()) { @@ -525,9 +554,17 @@ 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", debridlink: "Debrid-Link" + realdebrid: "Real-Debrid", megadebrid: "Mega-Debrid", bestdebrid: "BestDebrid", alldebrid: "AllDebrid", ddownload: "DDownload", onefichier: "1Fichier", debridlink: "Debrid-Link", linksnappy: "LinkSnappy" }; +function providerLabelWithMode(provider: DebridProvider, settings: AppSettings): string { + const base = providerLabels[provider]; + const kind = getConfiguredAccountKind(settings, provider); + if (!kind) return base; + const opt = ACCOUNT_OPTIONS.find((o) => o.kind === kind); + return opt?.modeLabel ? `${base} (${opt.modeLabel})` : base; +} + function formatDateTime(ts: number): string { if (!ts) return ""; const d = new Date(ts); @@ -1452,7 +1489,7 @@ export function App(): ReactElement { packages.length > 0 && packages.every((pkg) => collapsedPackages[pkg.id]) ), [packages, collapsedPackages]); - const configuredProviders = useMemo(() => getConfiguredProvidersFromSettings(settingsDraft), [settingsDraft]); + const configuredProviders = useMemo(() => getActiveProvidersFromSettings(settingsDraft), [settingsDraft]); // 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 @@ -1508,9 +1545,9 @@ export function App(): ReactElement { } else if (kind === "megadebrid-web") { note = "Web wird bevorzugt, API bleibt als Fallback aktiv."; } else if (kind === "realdebrid-web") { - note = "Login kann bei Bedarf direkt aus der Liste geoeffnet werden."; + note = "Login kann bei Bedarf direkt aus der Liste geöffnet werden."; } else if (kind === "bestdebrid-web") { - note = "Cookie-Import laesst sich direkt aus der Liste erneut starten."; + note = "Cookie-Import lässt sich direkt aus der Liste erneut starten."; } else if (service === "alldebrid") { if (allDebridHostLoading) { statusLabel = "Lade Status"; @@ -1525,14 +1562,20 @@ export function App(): ReactElement { note = "Status basiert auf den zuletzt gespeicherten AllDebrid-Daten."; } } + if (kind === "debridlink-api") { + const keyCount = (settingsDraft.debridLinkApiKeys || "").split(/[\n,]+/).filter((k: string) => k.trim()).length; + statusLabel = keyCount > 1 ? `${keyCount} API-Keys` : "Konfiguriert"; + } + const isDisabled = (settingsDraft.disabledProviders || []).includes(service as DebridProvider); entries.push({ kind, service, serviceLabel: option.serviceLabel, modeLabel: option.modeLabel, - statusLabel, + statusLabel: isDisabled ? "Deaktiviert" : statusLabel, summary: summarizeAccount(kind, settingsDraft), - note + note, + disabled: isDisabled }); } return entries; @@ -3586,7 +3629,7 @@ export function App(): ReactElement { const showQuickActionButton = Boolean(quickAction && !(showStatusButton && quickAction.action === "alldebrid-status")); const allDebridStateClass = entry.service === "alldebrid" && allDebridHostInfo ? ` account-status-${allDebridHostInfo.state}` : ""; return ( -
+
{entry.serviceLabel} {option.title} @@ -3612,6 +3655,14 @@ export function App(): ReactElement { {quickAction.label} )} + @@ -3628,18 +3679,18 @@ export function App(): ReactElement {

Hoster-Reihenfolge

-
Debrid-Accounts koennen hier priorisiert werden. Direkte Host-Accounts wie DDownload und 1Fichier laufen separat.
+
Debrid-Accounts können hier priorisiert werden. Direkte Host-Accounts wie DDownload und 1Fichier laufen separat.
{configuredProviders.length === 0 && (
Keine Debrid-Reihenfolge verfuegbar - Fuege mindestens einen Debrid-Account hinzu, dann kannst Du Hauptaccount und Alternativen festlegen. + Füge mindestens einen Debrid-Account hinzu, dann kannst Du Hauptaccount und Alternativen festlegen.
)} {configuredProviders.length >= 1 && (
)} @@ -3648,7 +3699,7 @@ export function App(): ReactElement {
)} @@ -3657,11 +3708,11 @@ export function App(): ReactElement {
)} - +
@@ -3760,19 +3811,19 @@ export function App(): ReactElement { )} {configuredProviders.length >= 1 && (
)} {configuredProviders.length >= 2 && (
)} {configuredProviders.length >= 3 && (
)} @@ -4051,8 +4102,8 @@ export function App(): ReactElement { {!accountDialogOption && (
- Oben zuerst einen Account-Typ waehlen - Danach erscheinen hier direkt die passenden Felder fuer Login, Passwort oder API-Token. + Oben zuerst einen Account-Typ wählen + Danach erscheinen hier direkt die passenden Felder für Login, Passwort oder API-Token.
)} @@ -4066,8 +4117,12 @@ export function App(): ReactElement {
{accountDialogOption.needsToken && (
- - setAccountDialog((prev) => prev ? { ...prev, token: event.target.value } : prev)} /> + + {accountDialogOption.service === "debridlink" ? ( +