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 <noreply@anthropic.com>
This commit is contained in:
Sucukdeluxe 2026-03-06 19:00:19 +01:00
parent a41c99e294
commit 22ed37d67c
6 changed files with 301 additions and 54 deletions

View File

@ -53,6 +53,8 @@ export function defaultSettings(): AppSettings {
ddownloadPassword: "", ddownloadPassword: "",
oneFichierApiKey: "", oneFichierApiKey: "",
debridLinkApiKeys: "", debridLinkApiKeys: "",
linkSnappyLogin: "",
linkSnappyPassword: "",
archivePasswordList: "", archivePasswordList: "",
rememberToken: true, rememberToken: true,
providerPrimary: "realdebrid", providerPrimary: "realdebrid",
@ -95,6 +97,7 @@ export function defaultSettings(): AppSettings {
bandwidthSchedules: [], bandwidthSchedules: [],
columnOrder: ["name", "size", "progress", "hoster", "account", "prio", "status", "speed"], columnOrder: ["name", "size", "progress", "hoster", "account", "prio", "status", "speed"],
extractCpuPriority: "high", extractCpuPriority: "high",
autoExtractWhenStopped: true autoExtractWhenStopped: true,
disabledProviders: []
}; };
} }

View File

@ -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_API_BASE = "https://debrid-link.com/api/v2";
const DEBRID_LINK_QUOTA_ERRORS = new Set(["maxLink", "maxLinkHost", "maxData", "maxDataHost", "maxAttempts", "maxTransfer"]); 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<DebridProvider, string> = { const PROVIDER_LABELS: Record<DebridProvider, string> = {
realdebrid: "Real-Debrid", realdebrid: "Real-Debrid",
megadebrid: "Mega-Debrid", megadebrid: "Mega-Debrid",
@ -27,7 +29,8 @@ const PROVIDER_LABELS: Record<DebridProvider, string> = {
alldebrid: "AllDebrid", alldebrid: "AllDebrid",
ddownload: "DDownload", ddownload: "DDownload",
onefichier: "1Fichier", onefichier: "1Fichier",
debridlink: "Debrid-Link" debridlink: "Debrid-Link",
linksnappy: "LinkSnappy"
}; };
interface ProviderUnrestrictedLink extends UnrestrictedLink { interface ProviderUnrestrictedLink extends UnrestrictedLink {
@ -1279,7 +1282,7 @@ class DebridLinkClient {
while (!triedAll) { while (!triedAll) {
const apiKey = this.apiKeys[this.currentKeyIndex]; 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 = ""; let lastError = "";
for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) { for (let attempt = 1; attempt <= REQUEST_RETRIES; attempt += 1) {
@ -1307,7 +1310,7 @@ class DebridLinkClient {
} }
if (errorCode === "badToken" || errorCode === "expired_token") { 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") { if (errorCode === "floodDetected") {
await sleep(retryDelay(attempt), signal); await sleep(retryDelay(attempt), signal);
@ -1330,19 +1333,21 @@ class DebridLinkClient {
const fileName = String(value.name || "") || filenameFromUrl(directUrl) || filenameFromUrl(link); const fileName = String(value.name || "") || filenameFromUrl(directUrl) || filenameFromUrl(link);
const fileSize = typeof value.size === "number" && value.size > 0 ? value.size : null; const fileSize = typeof value.size === "number" && value.size > 0 ? value.size : null;
logger.info(`Debrid-Link${keyLabel}: Unrestrict OK → ${fileName || "?"}`);
return { return {
fileName, fileName,
directUrl, directUrl,
fileSize, fileSize,
retriesUsed: attempt - 1, retriesUsed: attempt - 1,
sourceLabel: `API${keyLabel}` sourceLabel: keyLabel ? `#${this.currentKeyIndex + 1}` : "API"
}; };
} catch (error) { } catch (error) {
lastError = compactErrorText(error); lastError = compactErrorText(error);
if (signal?.aborted || (/aborted/i.test(lastError) && !/timeout/i.test(lastError))) { if (signal?.aborted || (/aborted/i.test(lastError) && !/timeout/i.test(lastError))) {
throw error; throw error;
} }
if (/Ungueltig|abgelaufen/i.test(lastError)) { if (/Ungültig|abgelaufen/i.test(lastError)) {
throw error; throw error;
} }
if (attempt < REQUEST_RETRIES) { 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<void> {
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<string, unknown>;
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<UnrestrictedLink> {
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<string, unknown>;
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<Record<string, unknown>> | 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<string, number> = { "": 1, K: 1024, M: 1024 ** 2, G: 1024 ** 3, T: 1024 ** 4 };
return Math.floor(num * (multipliers[unit] || 1));
}
// ── 1Fichier Client ── // ── 1Fichier Client ──
class OneFichierClient { class OneFichierClient {
@ -1620,6 +1769,8 @@ export class DebridService {
private cachedDdownloadKey = ""; private cachedDdownloadKey = "";
private cachedDebridLinkClient: DebridLinkClient | null = null; private cachedDebridLinkClient: DebridLinkClient | null = null;
private cachedDebridLinkKey = ""; private cachedDebridLinkKey = "";
private cachedLinkSnappyClient: LinkSnappyClient | null = null;
private cachedLinkSnappyKey = "";
public constructor(settings: AppSettings, options: DebridServiceOptions = {}) { public constructor(settings: AppSettings, options: DebridServiceOptions = {}) {
this.settings = cloneSettings(settings); this.settings = cloneSettings(settings);
@ -1639,6 +1790,16 @@ export class DebridService {
return this.cachedDebridLinkClient; 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 { private getDdownloadClient(login: string, password: string): DdownloadClient {
const key = `${login}\0${password}`; const key = `${login}\0${password}`;
if (this.cachedDdownloadClient && this.cachedDdownloadKey === key) { if (this.cachedDdownloadClient && this.cachedDdownloadKey === key) {
@ -1829,6 +1990,7 @@ export class DebridService {
} }
private isProviderConfiguredFor(settings: AppSettings, provider: DebridProvider): boolean { private isProviderConfiguredFor(settings: AppSettings, provider: DebridProvider): boolean {
if ((settings.disabledProviders || []).includes(provider)) return false;
if (provider === "realdebrid") { if (provider === "realdebrid") {
return Boolean(this.shouldUseRealDebridWeb(settings) || settings.token.trim()); return Boolean(this.shouldUseRealDebridWeb(settings) || settings.token.trim());
} }
@ -1847,6 +2009,9 @@ export class DebridService {
if (provider === "debridlink") { if (provider === "debridlink") {
return Boolean(settings.debridLinkApiKeys.trim()); return Boolean(settings.debridLinkApiKeys.trim());
} }
if (provider === "linksnappy") {
return Boolean(settings.linkSnappyLogin.trim() && settings.linkSnappyPassword.trim());
}
return Boolean(this.shouldUseBestDebridWeb(settings) || settings.bestToken.trim()); return Boolean(this.shouldUseBestDebridWeb(settings) || settings.bestToken.trim());
} }
@ -1891,6 +2056,9 @@ export class DebridService {
dlResult.sourceLabel = dlResult.sourceLabel || "API"; dlResult.sourceLabel = dlResult.sourceLabel || "API";
return dlResult; return dlResult;
} }
if (provider === "linksnappy") {
return this.getLinkSnappyClient(settings.linkSnappyLogin, settings.linkSnappyPassword).unrestrictLink(link, signal);
}
if (this.shouldUseBestDebridWeb(settings) && this.options.bestDebridWebUnrestrict) { if (this.shouldUseBestDebridWeb(settings) && this.options.bestDebridWebUnrestrict) {
const bdResult = await this.options.bestDebridWebUnrestrict(link, signal); const bdResult = await this.options.bestDebridWebUnrestrict(link, signal);
if (!bdResult) { if (!bdResult) {

View File

@ -5,8 +5,8 @@ import { AppSettings, BandwidthScheduleEntry, DebridProvider, DownloadItem, Down
import { defaultSettings } from "./constants"; import { defaultSettings } from "./constants";
import { logger } from "./logger"; import { logger } from "./logger";
const VALID_PRIMARY_PROVIDERS = new Set(["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"]); 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_CLEANUP_MODES = new Set(["none", "trash", "delete"]);
const VALID_CONFLICT_MODES = new Set(["overwrite", "skip", "rename", "ask"]); const VALID_CONFLICT_MODES = new Set(["overwrite", "skip", "rename", "ask"]);
const VALID_FINISHED_POLICIES = new Set(["never", "immediate", "on_start", "package_done"]); 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), ddownloadPassword: asText(settings.ddownloadPassword),
oneFichierApiKey: asText(settings.oneFichierApiKey), oneFichierApiKey: asText(settings.oneFichierApiKey),
debridLinkApiKeys: String(settings.debridLinkApiKeys ?? "").replace(/\r\n|\r/g, "\n").trim(), 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"), archivePasswordList: String(settings.archivePasswordList ?? "").replace(/\r\n|\r/g, "\n"),
rememberToken: Boolean(settings.rememberToken), rememberToken: Boolean(settings.rememberToken),
providerPrimary: settings.providerPrimary, providerPrimary: settings.providerPrimary,
@ -161,7 +163,8 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
bandwidthSchedules: normalizeBandwidthSchedules(settings.bandwidthSchedules), bandwidthSchedules: normalizeBandwidthSchedules(settings.bandwidthSchedules),
columnOrder: normalizeColumnOrder(settings.columnOrder), columnOrder: normalizeColumnOrder(settings.columnOrder),
extractCpuPriority: settings.extractCpuPriority, 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)) { if (!VALID_PRIMARY_PROVIDERS.has(normalized.providerPrimary)) {
@ -214,7 +217,9 @@ function sanitizeCredentialPersistence(settings: AppSettings): AppSettings {
ddownloadLogin: "", ddownloadLogin: "",
ddownloadPassword: "", ddownloadPassword: "",
oneFichierApiKey: "", oneFichierApiKey: "",
debridLinkApiKeys: "" debridLinkApiKeys: "",
linkSnappyLogin: "",
linkSnappyPassword: ""
}; };
} }

View File

@ -53,7 +53,7 @@ interface LinkPopupState {
isPackage: boolean; isPackage: boolean;
} }
type AccountService = "realdebrid" | "megadebrid" | "bestdebrid" | "alldebrid" | "ddownload" | "onefichier" | "debridlink"; type AccountService = "realdebrid" | "megadebrid" | "bestdebrid" | "alldebrid" | "ddownload" | "onefichier" | "debridlink" | "linksnappy";
type AccountKind = type AccountKind =
| "realdebrid-api" | "realdebrid-api"
| "realdebrid-web" | "realdebrid-web"
@ -65,7 +65,8 @@ type AccountKind =
| "alldebrid-web" | "alldebrid-web"
| "ddownload-login" | "ddownload-login"
| "onefichier-api" | "onefichier-api"
| "debridlink-api"; | "debridlink-api"
| "linksnappy-login";
type AccountQuickAction = "realdebrid-login" | "bestdebrid-cookies" | "alldebrid-login" | "alldebrid-status"; type AccountQuickAction = "realdebrid-login" | "bestdebrid-cookies" | "alldebrid-login" | "alldebrid-status";
type AccountColumnKey = "service" | "mode" | "status" | "secret"; type AccountColumnKey = "service" | "mode" | "status" | "secret";
@ -97,6 +98,7 @@ interface ConfiguredAccountEntry {
statusLabel: string; statusLabel: string;
summary: string; summary: string;
note: string; note: string;
disabled: boolean;
} }
const ACCOUNT_OPTIONS: AccountOption[] = [ const ACCOUNT_OPTIONS: AccountOption[] = [
@ -106,7 +108,7 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
serviceLabel: "Real-Debrid", serviceLabel: "Real-Debrid",
title: "Real-Debrid API", title: "Real-Debrid API",
modeLabel: "API", modeLabel: "API",
pickerDescription: "Direkter Zugriff ueber API-Token.", pickerDescription: "Direkter Zugriff über API-Token.",
needsToken: true needsToken: true
}, },
{ {
@ -115,7 +117,7 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
serviceLabel: "Real-Debrid", serviceLabel: "Real-Debrid",
title: "Real-Debrid Web", title: "Real-Debrid Web",
modeLabel: "Web", modeLabel: "Web",
pickerDescription: "Login ueber Browserfenster statt Token." pickerDescription: "Login über Browserfenster statt Token."
}, },
{ {
kind: "megadebrid-api", kind: "megadebrid-api",
@ -123,7 +125,7 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
serviceLabel: "Mega-Debrid", serviceLabel: "Mega-Debrid",
title: "Mega-Debrid API", title: "Mega-Debrid API",
modeLabel: "API", modeLabel: "API",
pickerDescription: "Login mit API-Praeferenz und Web-Fallback.", pickerDescription: "Login mit API-Präferenz und Web-Fallback.",
needsCredentials: true needsCredentials: true
}, },
{ {
@ -132,7 +134,7 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
serviceLabel: "Mega-Debrid", serviceLabel: "Mega-Debrid",
title: "Mega-Debrid Web", title: "Mega-Debrid Web",
modeLabel: "Web", modeLabel: "Web",
pickerDescription: "Login mit Web-Praeferenz ueber Nutzername und Passwort.", pickerDescription: "Login mit Web-Präferenz über Nutzername und Passwort.",
needsCredentials: true needsCredentials: true
}, },
{ {
@ -141,7 +143,7 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
serviceLabel: "BestDebrid", serviceLabel: "BestDebrid",
title: "BestDebrid API", title: "BestDebrid API",
modeLabel: "API", modeLabel: "API",
pickerDescription: "Direkter Zugriff ueber API-Token.", pickerDescription: "Direkter Zugriff über API-Token.",
needsToken: true needsToken: true
}, },
{ {
@ -158,7 +160,7 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
serviceLabel: "AllDebrid", serviceLabel: "AllDebrid",
title: "AllDebrid API", title: "AllDebrid API",
modeLabel: "API", modeLabel: "API",
pickerDescription: "Direkter Zugriff ueber API-Key.", pickerDescription: "Direkter Zugriff über API-Key.",
needsToken: true needsToken: true
}, },
{ {
@ -167,7 +169,7 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
serviceLabel: "AllDebrid", serviceLabel: "AllDebrid",
title: "AllDebrid Web", title: "AllDebrid Web",
modeLabel: "Web", modeLabel: "Web",
pickerDescription: "Login ueber Browserfenster fuer reCAPTCHA.", pickerDescription: "Login über Browserfenster für reCAPTCHA.",
}, },
{ {
kind: "ddownload-login", kind: "ddownload-login",
@ -175,7 +177,7 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
serviceLabel: "DDownload", serviceLabel: "DDownload",
title: "DDownload Login", title: "DDownload Login",
modeLabel: "Login", modeLabel: "Login",
pickerDescription: "Direkter Login fuer ddownload.com und ddl.to.", pickerDescription: "Direkter Login für ddownload.com und ddl.to.",
needsCredentials: true needsCredentials: true
}, },
{ {
@ -184,7 +186,7 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
serviceLabel: "1Fichier", serviceLabel: "1Fichier",
title: "1Fichier API", title: "1Fichier API",
modeLabel: "API", modeLabel: "API",
pickerDescription: "API-Key fuer 1fichier.com.", pickerDescription: "API-Key für 1fichier.com.",
needsToken: true needsToken: true
}, },
{ {
@ -193,12 +195,21 @@ const ACCOUNT_OPTIONS: AccountOption[] = [
serviceLabel: "Debrid-Link", serviceLabel: "Debrid-Link",
title: "Debrid-Link API", title: "Debrid-Link API",
modeLabel: "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 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_STORAGE_KEY = "rd-account-column-widths";
const ACCOUNT_COLUMN_DEFAULT_WIDTHS: Record<AccountColumnKey, number> = { const ACCOUNT_COLUMN_DEFAULT_WIDTHS: Record<AccountColumnKey, number> = {
service: 220, service: 220,
@ -283,11 +294,19 @@ function getConfiguredProvidersFromSettings(settings: AppSettings): DebridProvid
if ((settings.debridLinkApiKeys || "").trim()) { if ((settings.debridLinkApiKeys || "").trim()) {
list.push("debridlink"); list.push("debridlink");
} }
if ((settings.linkSnappyLogin || "").trim() && (settings.linkSnappyPassword || "").trim()) {
list.push("linksnappy");
}
return list; 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<AppSettings, "providerPrimary" | "providerSecondary" | "providerTertiary"> { function normalizeProviderSelectionForSettings(settings: AppSettings): Pick<AppSettings, "providerPrimary" | "providerSecondary" | "providerTertiary"> {
const configuredProviders = getConfiguredProvidersFromSettings(settings); const configuredProviders = getActiveProvidersFromSettings(settings);
const primaryProvider = configuredProviders.includes(settings.providerPrimary) const primaryProvider = configuredProviders.includes(settings.providerPrimary)
? settings.providerPrimary ? settings.providerPrimary
: (configuredProviders[0] ?? "realdebrid"); : (configuredProviders[0] ?? "realdebrid");
@ -326,6 +345,8 @@ function getConfiguredAccountKind(settings: AppSettings, service: AccountService
return settings.oneFichierApiKey.trim() ? "onefichier-api" : null; return settings.oneFichierApiKey.trim() ? "onefichier-api" : null;
case "debridlink": case "debridlink":
return (settings.debridLinkApiKeys || "").trim() ? "debridlink-api" : null; return (settings.debridLinkApiKeys || "").trim() ? "debridlink-api" : null;
case "linksnappy":
return (settings.linkSnappyLogin || "").trim() && (settings.linkSnappyPassword || "").trim() ? "linksnappy-login" : null;
default: default:
return null; return null;
} }
@ -366,6 +387,8 @@ function summarizeAccount(kind: AccountKind, settings: AppSettings): string {
if (keys.length > 1) return `${keys.length} API-Keys`; if (keys.length > 1) return `${keys.length} API-Keys`;
return keys.length === 1 ? maskValue(keys[0].trim(), 3, 3) : "Nicht hinterlegt"; 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: default:
return "Konfiguriert"; return "Konfiguriert";
} }
@ -403,6 +426,8 @@ function createAccountDialogState(mode: "create" | "edit", kind: AccountKind | n
return { mode, kind, token: settings.oneFichierApiKey, login: "", password: "" }; return { mode, kind, token: settings.oneFichierApiKey, login: "", password: "" };
case "debridlink-api": case "debridlink-api":
return { mode, kind, token: settings.debridLinkApiKeys || "", login: "", password: "" }; return { mode, kind, token: settings.debridLinkApiKeys || "", login: "", password: "" };
case "linksnappy-login":
return { mode, kind, token: "", login: settings.linkSnappyLogin || "", password: settings.linkSnappyPassword || "" };
default: default:
return { mode, kind, token: "", login: "", password: "" }; return { mode, kind, token: "", login: "", password: "" };
} }
@ -438,6 +463,8 @@ function applyAccountDialogToSettings(settings: AppSettings, dialog: AccountDial
return { ...settings, oneFichierApiKey: token }; return { ...settings, oneFichierApiKey: token };
case "debridlink-api": case "debridlink-api":
return { ...settings, debridLinkApiKeys: token }; return { ...settings, debridLinkApiKeys: token };
case "linksnappy-login":
return { ...settings, linkSnappyLogin: login, linkSnappyPassword: password };
default: default:
return settings; return settings;
} }
@ -459,6 +486,8 @@ function clearAccountServiceFromSettings(settings: AppSettings, service: Account
return { ...settings, oneFichierApiKey: "" }; return { ...settings, oneFichierApiKey: "" };
case "debridlink": case "debridlink":
return { ...settings, debridLinkApiKeys: "" }; return { ...settings, debridLinkApiKeys: "" };
case "linksnappy":
return { ...settings, linkSnappyLogin: "", linkSnappyPassword: "" };
default: default:
return settings; return settings;
} }
@ -466,7 +495,7 @@ function clearAccountServiceFromSettings(settings: AppSettings, service: Account
function validateAccountDialog(dialog: AccountDialogState): string | null { function validateAccountDialog(dialog: AccountDialogState): string | null {
if (!dialog.kind) { if (!dialog.kind) {
return "Bitte zuerst einen Account-Typ auswaehlen."; return "Bitte zuerst einen Account-Typ auswählen.";
} }
const option = findAccountOption(dialog.kind); const option = findAccountOption(dialog.kind);
if (option.needsToken && !dialog.token.trim()) { if (option.needsToken && !dialog.token.trim()) {
@ -525,9 +554,17 @@ const cleanupLabels: Record<string, string> = {
const AUTO_RENDER_PACKAGE_LIMIT = 260; const AUTO_RENDER_PACKAGE_LIMIT = 260;
const providerLabels: Record<DebridProvider, string> = { const providerLabels: Record<DebridProvider, string> = {
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 { function formatDateTime(ts: number): string {
if (!ts) return ""; if (!ts) return "";
const d = new Date(ts); const d = new Date(ts);
@ -1452,7 +1489,7 @@ export function App(): ReactElement {
packages.length > 0 && packages.every((pkg) => collapsedPackages[pkg.id]) packages.length > 0 && packages.every((pkg) => collapsedPackages[pkg.id])
), [packages, collapsedPackages]); ), [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 // 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
@ -1508,9 +1545,9 @@ export function App(): ReactElement {
} else if (kind === "megadebrid-web") { } else if (kind === "megadebrid-web") {
note = "Web wird bevorzugt, API bleibt als Fallback aktiv."; note = "Web wird bevorzugt, API bleibt als Fallback aktiv.";
} else if (kind === "realdebrid-web") { } 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") { } 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") { } else if (service === "alldebrid") {
if (allDebridHostLoading) { if (allDebridHostLoading) {
statusLabel = "Lade Status"; statusLabel = "Lade Status";
@ -1525,14 +1562,20 @@ export function App(): ReactElement {
note = "Status basiert auf den zuletzt gespeicherten AllDebrid-Daten."; 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({ entries.push({
kind, kind,
service, service,
serviceLabel: option.serviceLabel, serviceLabel: option.serviceLabel,
modeLabel: option.modeLabel, modeLabel: option.modeLabel,
statusLabel, statusLabel: isDisabled ? "Deaktiviert" : statusLabel,
summary: summarizeAccount(kind, settingsDraft), summary: summarizeAccount(kind, settingsDraft),
note note,
disabled: isDisabled
}); });
} }
return entries; return entries;
@ -3586,7 +3629,7 @@ export function App(): ReactElement {
const showQuickActionButton = Boolean(quickAction && !(showStatusButton && quickAction.action === "alldebrid-status")); const showQuickActionButton = Boolean(quickAction && !(showStatusButton && quickAction.action === "alldebrid-status"));
const allDebridStateClass = entry.service === "alldebrid" && allDebridHostInfo ? ` account-status-${allDebridHostInfo.state}` : ""; const allDebridStateClass = entry.service === "alldebrid" && allDebridHostInfo ? ` account-status-${allDebridHostInfo.state}` : "";
return ( return (
<div key={entry.service} className="account-row"> <div key={entry.service} className={`account-row${entry.disabled ? " account-row-disabled" : ""}`}>
<div className="account-cell account-service-cell"> <div className="account-cell account-service-cell">
<strong>{entry.serviceLabel}</strong> <strong>{entry.serviceLabel}</strong>
<span>{option.title}</span> <span>{option.title}</span>
@ -3612,6 +3655,14 @@ export function App(): ReactElement {
{quickAction.label} {quickAction.label}
</button> </button>
)} )}
<button className="btn" disabled={actionBusy} onClick={() => {
const provider = entry.service as DebridProvider;
const current = settingsDraft.disabledProviders || [];
const next = current.includes(provider) ? current.filter((p) => p !== provider) : [...current, provider];
setSettingsDraft((prev) => ({ ...prev, disabledProviders: next }));
}}>
{entry.disabled ? "Aktivieren" : "Deaktivieren"}
</button>
<button className="btn" disabled={actionBusy} onClick={() => openEditAccountDialog(entry.kind)}> <button className="btn" disabled={actionBusy} onClick={() => openEditAccountDialog(entry.kind)}>
Bearbeiten Bearbeiten
</button> </button>
@ -3628,18 +3679,18 @@ export function App(): ReactElement {
<div className="settings-section card"> <div className="settings-section card">
<h3>Hoster-Reihenfolge</h3> <h3>Hoster-Reihenfolge</h3>
<div className="hint">Debrid-Accounts koennen hier priorisiert werden. Direkte Host-Accounts wie DDownload und 1Fichier laufen separat.</div> <div className="hint">Debrid-Accounts können hier priorisiert werden. Direkte Host-Accounts wie DDownload und 1Fichier laufen separat.</div>
{configuredProviders.length === 0 && ( {configuredProviders.length === 0 && (
<div className="account-empty-state compact"> <div className="account-empty-state compact">
<strong>Keine Debrid-Reihenfolge verfuegbar</strong> <strong>Keine Debrid-Reihenfolge verfuegbar</strong>
<span>Fuege mindestens einen Debrid-Account hinzu, dann kannst Du Hauptaccount und Alternativen festlegen.</span> <span>Füge mindestens einen Debrid-Account hinzu, dann kannst Du Hauptaccount und Alternativen festlegen.</span>
</div> </div>
)} )}
{configuredProviders.length >= 1 && ( {configuredProviders.length >= 1 && (
<div> <div>
<label>Hauptaccount</label> <label>Hauptaccount</label>
<select value={primaryProviderValue} onChange={(e) => setText("providerPrimary", e.target.value)}> <select value={primaryProviderValue} onChange={(e) => setText("providerPrimary", e.target.value)}>
{configuredProviders.map((provider) => (<option key={provider} value={provider}>{providerLabels[provider]}</option>))} {configuredProviders.map((provider) => (<option key={provider} value={provider}>{providerLabelWithMode(provider, settingsDraft)}</option>))}
</select> </select>
</div> </div>
)} )}
@ -3648,7 +3699,7 @@ export function App(): ReactElement {
<label>1. Hoster-Alternative</label> <label>1. Hoster-Alternative</label>
<select value={secondaryProviderValue} onChange={(e) => setText("providerSecondary", e.target.value)}> <select value={secondaryProviderValue} onChange={(e) => setText("providerSecondary", e.target.value)}>
<option value="none">Keine Alternative</option> <option value="none">Keine Alternative</option>
{secondaryProviderChoices.map((provider) => (<option key={provider} value={provider}>{providerLabels[provider]}</option>))} {secondaryProviderChoices.map((provider) => (<option key={provider} value={provider}>{providerLabelWithMode(provider, settingsDraft)}</option>))}
</select> </select>
</div> </div>
)} )}
@ -3657,11 +3708,11 @@ export function App(): ReactElement {
<label>2. Hoster-Alternative</label> <label>2. Hoster-Alternative</label>
<select value={tertiaryProviderValue} onChange={(e) => setText("providerTertiary", e.target.value)}> <select value={tertiaryProviderValue} onChange={(e) => setText("providerTertiary", e.target.value)}>
<option value="none">Keine Alternative</option> <option value="none">Keine Alternative</option>
{tertiaryProviderChoices.map((provider) => (<option key={provider} value={provider}>{providerLabels[provider]}</option>))} {tertiaryProviderChoices.map((provider) => (<option key={provider} value={provider}>{providerLabelWithMode(provider, settingsDraft)}</option>))}
</select> </select>
</div> </div>
)} )}
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.autoProviderFallback} onChange={(e) => setBool("autoProviderFallback", e.target.checked)} /> Bei Fehlern oder Fair-Use automatisch zum naechsten Provider wechseln</label> <label className="toggle-line"><input type="checkbox" checked={settingsDraft.autoProviderFallback} onChange={(e) => setBool("autoProviderFallback", e.target.checked)} /> Bei Fehlern oder Fair-Use automatisch zum nächsten Provider wechseln</label>
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.rememberToken} onChange={(e) => setBool("rememberToken", e.target.checked)} /> Zugangsdaten lokal speichern</label> <label className="toggle-line"><input type="checkbox" checked={settingsDraft.rememberToken} onChange={(e) => setBool("rememberToken", e.target.checked)} /> Zugangsdaten lokal speichern</label>
</div> </div>
@ -3760,19 +3811,19 @@ export function App(): ReactElement {
)} )}
{configuredProviders.length >= 1 && ( {configuredProviders.length >= 1 && (
<div><label>Hauptaccount</label><select value={primaryProviderValue} onChange={(e) => setText("providerPrimary", e.target.value)}> <div><label>Hauptaccount</label><select value={primaryProviderValue} onChange={(e) => setText("providerPrimary", e.target.value)}>
{configuredProviders.map((provider) => (<option key={provider} value={provider}>{providerLabels[provider]}</option>))} {configuredProviders.map((provider) => (<option key={provider} value={provider}>{providerLabelWithMode(provider, settingsDraft)}</option>))}
</select></div> </select></div>
)} )}
{configuredProviders.length >= 2 && ( {configuredProviders.length >= 2 && (
<div><label>1. Hoster-Alternative</label><select value={secondaryProviderValue} onChange={(e) => setText("providerSecondary", e.target.value)}> <div><label>1. Hoster-Alternative</label><select value={secondaryProviderValue} onChange={(e) => setText("providerSecondary", e.target.value)}>
<option value="none">Keine Alternative</option> <option value="none">Keine Alternative</option>
{secondaryProviderChoices.map((provider) => (<option key={provider} value={provider}>{providerLabels[provider]}</option>))} {secondaryProviderChoices.map((provider) => (<option key={provider} value={provider}>{providerLabelWithMode(provider, settingsDraft)}</option>))}
</select></div> </select></div>
)} )}
{configuredProviders.length >= 3 && ( {configuredProviders.length >= 3 && (
<div><label>2. Hoster-Alternative</label><select value={tertiaryProviderValue} onChange={(e) => setText("providerTertiary", e.target.value)}> <div><label>2. Hoster-Alternative</label><select value={tertiaryProviderValue} onChange={(e) => setText("providerTertiary", e.target.value)}>
<option value="none">Keine Alternative</option> <option value="none">Keine Alternative</option>
{tertiaryProviderChoices.map((provider) => (<option key={provider} value={provider}>{providerLabels[provider]}</option>))} {tertiaryProviderChoices.map((provider) => (<option key={provider} value={provider}>{providerLabelWithMode(provider, settingsDraft)}</option>))}
</select></div> </select></div>
)} )}
<label className="toggle-line"><input type="checkbox" checked={settingsDraft.autoProviderFallback} onChange={(e) => setBool("autoProviderFallback", e.target.checked)} /> Bei Fehler/Fair-Use automatisch zum nächsten Provider wechseln</label> <label className="toggle-line"><input type="checkbox" checked={settingsDraft.autoProviderFallback} onChange={(e) => setBool("autoProviderFallback", e.target.checked)} /> Bei Fehler/Fair-Use automatisch zum nächsten Provider wechseln</label>
@ -4051,8 +4102,8 @@ export function App(): ReactElement {
{!accountDialogOption && ( {!accountDialogOption && (
<div className="account-empty-state compact"> <div className="account-empty-state compact">
<strong>Oben zuerst einen Account-Typ waehlen</strong> <strong>Oben zuerst einen Account-Typ wählen</strong>
<span>Danach erscheinen hier direkt die passenden Felder fuer Login, Passwort oder API-Token.</span> <span>Danach erscheinen hier direkt die passenden Felder für Login, Passwort oder API-Token.</span>
</div> </div>
)} )}
@ -4066,8 +4117,12 @@ export function App(): ReactElement {
<div className="account-modal-fields"> <div className="account-modal-fields">
{accountDialogOption.needsToken && ( {accountDialogOption.needsToken && (
<div> <div>
<label>{accountDialogOption.service === "alldebrid" || accountDialogOption.service === "onefichier" ? "API-Key" : "Token"}</label> <label>{accountDialogOption.service === "alldebrid" || accountDialogOption.service === "onefichier" || accountDialogOption.service === "debridlink" ? "API-Key(s)" : "Token"}</label>
<input type="password" value={accountDialog.token} onChange={(event) => setAccountDialog((prev) => prev ? { ...prev, token: event.target.value } : prev)} /> {accountDialogOption.service === "debridlink" ? (
<textarea rows={4} placeholder="Ein API-Key pro Zeile" value={accountDialog.token} onChange={(event) => setAccountDialog((prev) => prev ? { ...prev, token: event.target.value } : prev)} style={{ fontFamily: "monospace", resize: "vertical" }} />
) : (
<input type="password" value={accountDialog.token} onChange={(event) => setAccountDialog((prev) => prev ? { ...prev, token: event.target.value } : prev)} />
)}
</div> </div>
)} )}
@ -4085,7 +4140,7 @@ export function App(): ReactElement {
)} )}
{accountDialog.kind === "realdebrid-web" && ( {accountDialog.kind === "realdebrid-web" && (
<div className="account-modal-note">Nach dem Speichern kannst Du direkt das Browserfenster fuer den Web-Login oeffnen.</div> <div className="account-modal-note">Nach dem Speichern kannst Du direkt das Browserfenster für den Web-Login öffnen.</div>
)} )}
{accountDialog.kind === "bestdebrid-web" && ( {accountDialog.kind === "bestdebrid-web" && (
<div className="account-modal-note">Der Web-Account arbeitet ueber einen Cookies.txt-Import aus dem Browser.</div> <div className="account-modal-note">Der Web-Account arbeitet ueber einen Cookies.txt-Import aus dem Browser.</div>

View File

@ -1141,8 +1141,8 @@ body,
minmax(180px, var(--account-col-service)) minmax(180px, var(--account-col-service))
minmax(80px, var(--account-col-mode)) minmax(80px, var(--account-col-mode))
minmax(180px, var(--account-col-status)) minmax(180px, var(--account-col-status))
minmax(140px, var(--account-col-secret)) minmax(120px, var(--account-col-secret))
minmax(190px, 1fr); minmax(260px, 1fr);
gap: 10px; gap: 10px;
align-items: center; align-items: center;
width: 100%; width: 100%;
@ -1287,18 +1287,26 @@ body,
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
align-items: flex-start; align-items: flex-start;
gap: 8px; gap: 6px;
flex-wrap: wrap; flex-wrap: nowrap;
} }
.account-row-actions .btn { .account-row-actions .btn {
padding: 6px 10px; padding: 5px 8px;
font-size: 12px; font-size: 11px;
white-space: nowrap; white-space: nowrap;
} }
.account-row-disabled {
opacity: 0.45;
}
.account-row-disabled .account-row-actions {
opacity: 1;
}
.account-modal { .account-modal {
width: min(880px, calc(100vw - 36px)); width: min(960px, calc(100vw - 36px));
max-height: calc(100vh - 36px); max-height: calc(100vh - 36px);
overflow: hidden; overflow: hidden;
gap: 14px; gap: 14px;
@ -1401,6 +1409,11 @@ body,
letter-spacing: 0.02em; letter-spacing: 0.02em;
} }
.account-modal textarea {
white-space: nowrap;
overflow-x: auto;
}
.account-picker-table { .account-picker-table {
display: grid; display: grid;
border: 1px solid color-mix(in srgb, var(--border) 84%, transparent); border: 1px solid color-mix(in srgb, var(--border) 84%, transparent);

View File

@ -14,7 +14,7 @@ export type CleanupMode = "none" | "trash" | "delete";
export type ConflictMode = "overwrite" | "skip" | "rename" | "ask"; export type ConflictMode = "overwrite" | "skip" | "rename" | "ask";
export type SpeedMode = "global" | "per_download"; export type SpeedMode = "global" | "per_download";
export type FinishedCleanupPolicy = "never" | "immediate" | "on_start" | "package_done"; export type FinishedCleanupPolicy = "never" | "immediate" | "on_start" | "package_done";
export type DebridProvider = "realdebrid" | "megadebrid" | "bestdebrid" | "alldebrid" | "ddownload" | "onefichier" | "debridlink"; export type DebridProvider = "realdebrid" | "megadebrid" | "bestdebrid" | "alldebrid" | "ddownload" | "onefichier" | "debridlink" | "linksnappy";
export type DebridFallbackProvider = DebridProvider | "none"; export type DebridFallbackProvider = DebridProvider | "none";
export type AppTheme = "dark" | "light"; export type AppTheme = "dark" | "light";
export type PackagePriority = "high" | "normal" | "low"; export type PackagePriority = "high" | "normal" | "low";
@ -50,6 +50,8 @@ export interface AppSettings {
ddownloadPassword: string; ddownloadPassword: string;
oneFichierApiKey: string; oneFichierApiKey: string;
debridLinkApiKeys: string; debridLinkApiKeys: string;
linkSnappyLogin: string;
linkSnappyPassword: string;
archivePasswordList: string; archivePasswordList: string;
rememberToken: boolean; rememberToken: boolean;
providerPrimary: DebridProvider; providerPrimary: DebridProvider;
@ -93,6 +95,7 @@ export interface AppSettings {
columnOrder: string[]; columnOrder: string[];
extractCpuPriority: ExtractCpuPriority; extractCpuPriority: ExtractCpuPriority;
autoExtractWhenStopped: boolean; autoExtractWhenStopped: boolean;
disabledProviders: DebridProvider[];
} }
export interface DownloadItem { export interface DownloadItem {