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:
parent
a41c99e294
commit
22ed37d67c
@ -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: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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: ""
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user