Files
Multi-Debrid-Downloader/src/main/account-commands.ts
T
Sucukdeluxe 1b7caba2eb feat: add Deepbrid, daily scheduling, and notification center
Add encrypted Deepbrid API accounts with account validation, provider routing, fallback, usage tracking, safe error handling, and verified 1Fichier downloads. Restore persistent recurring daily starts with local-calendar deduplication and legacy schedule compatibility. Add durable Discord package, run, remaining-volume, stall, and recovery notifications with privacy-safe telemetry and disk-failure recovery.
2026-08-24 07:37:55 +02:00

650 lines
32 KiB
TypeScript

import { getDebridLinkApiKeyId, parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
import { randomUUID } from "node:crypto";
import {
getMegaDebridAccountId,
getMegaDebridAccountsForMode,
getMegaDebridCredentialsForMode,
getMegaDebridDisabledAccountIdsForMode,
mergeMegaDebridCredentialPools,
parseMegaDebridAccounts,
serializeMegaDebridAccounts,
type MegaDebridAccountMode
} from "../shared/mega-debrid-accounts";
import { getRealDebridAccounts, isRealDebridWebAccountId, parseRealDebridApiAccounts, serializeRealDebridApiAccounts } from "../shared/real-debrid-accounts";
import type { AccountCommand, AccountCredentialCheckInput, AccountSecretRequest, AppSettings, DebridProvider, RendererAccountKind } from "../shared/types";
export interface AppliedAccountCommand {
settings: AppSettings;
response: { accountId: string | null };
}
const ACCOUNT_KINDS = new Set<RendererAccountKind>([
"realdebrid-api",
"realdebrid-web",
"megadebrid-api",
"megadebrid-web",
"bestdebrid-api",
"bestdebrid-web",
"alldebrid-api",
"alldebrid-web",
"deepbrid-api",
"ddownload-login",
"onefichier-api",
"debridlink-api",
"linksnappy-login"
]);
const ACTION_FIELDS: Record<AccountCommand["action"], ReadonlySet<string>> = {
create: new Set(["action", "kind", "identity", "secret", "dailyLimitBytes"]),
replace: new Set(["action", "kind", "accountId", "identity", "secret", "dailyLimitBytes"]),
"update-secret": new Set(["action", "kind", "accountId", "secret"]),
delete: new Set(["action", "kind", "accountId"])
};
function invalid(): never {
throw new Error("Account-Payload ist ungültig");
}
function optionalString(value: unknown, maxLength: number): string | undefined {
if (value === undefined) {
return undefined;
}
if (typeof value !== "string" || value.length > maxLength) {
invalid();
}
return value;
}
function requiredString(value: unknown, maxLength: number): string {
const result = optionalString(value, maxLength);
if (result === undefined || !result.trim()) {
invalid();
}
return result;
}
function optionalLimit(value: unknown): number | undefined {
if (value === undefined) {
return undefined;
}
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
invalid();
}
return value;
}
export function validateAccountCommand(value: unknown): AccountCommand {
if (!value || typeof value !== "object" || Array.isArray(value)) {
invalid();
}
const raw = value as Record<string, unknown>;
const action = raw.action;
if (action !== "create" && action !== "replace" && action !== "update-secret" && action !== "delete") {
invalid();
}
if (Object.keys(raw).some((key) => !ACTION_FIELDS[action].has(key))) {
invalid();
}
if (typeof raw.kind !== "string" || !ACCOUNT_KINDS.has(raw.kind as RendererAccountKind)) {
invalid();
}
const kind = raw.kind as RendererAccountKind;
if (action === "create") {
return {
action,
kind,
identity: optionalString(raw.identity, 512),
secret: optionalString(raw.secret, 100_000),
dailyLimitBytes: optionalLimit(raw.dailyLimitBytes)
};
}
const accountId = requiredString(raw.accountId, 256).trim();
if (action === "replace") {
return {
action,
kind,
accountId,
identity: optionalString(raw.identity, 512),
secret: optionalString(raw.secret, 100_000),
dailyLimitBytes: optionalLimit(raw.dailyLimitBytes)
};
}
if (action === "update-secret") {
return {
action,
kind,
accountId,
secret: requiredString(raw.secret, 100_000)
};
}
return { action, kind, accountId };
}
export function validateAccountCredentialCheckInput(value: unknown): AccountCredentialCheckInput {
if (!value || typeof value !== "object" || Array.isArray(value)) invalid();
const raw = value as Record<string, unknown>;
if (Object.keys(raw).some((key) => !new Set(["kind", "accountId", "identity", "secret"]).has(key))) invalid();
if (raw.kind !== "realdebrid-api"
&& raw.kind !== "realdebrid-web"
&& raw.kind !== "megadebrid-api"
&& raw.kind !== "megadebrid-web"
&& raw.kind !== "debridlink-api"
&& raw.kind !== "deepbrid-api") invalid();
const accountId = optionalString(raw.accountId, 256);
if (raw.kind === "deepbrid-api" && accountId !== undefined && accountId !== "svc-deepbrid") invalid();
return {
kind: raw.kind,
accountId,
identity: optionalString(raw.identity, 512),
secret: optionalString(raw.secret, 100_000)
};
}
export function validateAccountSecretRequest(value: unknown): AccountSecretRequest {
if (!value || typeof value !== "object" || Array.isArray(value)) invalid();
const raw = value as Record<string, unknown>;
if (Object.keys(raw).some((key) => key !== "kind" && key !== "accountId")) invalid();
if (typeof raw.kind !== "string" || !ACCOUNT_KINDS.has(raw.kind as RendererAccountKind)) invalid();
return {
kind: raw.kind as RendererAccountKind,
accountId: requiredString(raw.accountId, 256).trim()
};
}
function storedSecretMissing(): never {
throw new Error("Der gespeicherte Zugang wurde nicht gefunden.");
}
export function resolveStoredAccountSecret(settings: AppSettings, request: AccountSecretRequest): string {
if (request.kind === "realdebrid-api") {
const accounts = parseRealDebridApiAccounts(settings.realDebridApiTokens);
const account = accounts.find((entry) => entry.id === request.accountId);
if (account?.token) return account.token;
if (accounts.length === 0 && request.accountId === REAL_DEBRID_LEGACY_ID && settings.token) return settings.token;
return storedSecretMissing();
}
if (request.kind === "megadebrid-api" || request.kind === "megadebrid-web") {
const mode = request.kind === "megadebrid-web" ? "web" : "api";
const account = getMegaDebridAccountsForMode(settings, mode).find((entry) => entry.id === request.accountId);
if (!account?.password) storedSecretMissing();
return account.password;
}
if (request.kind === "debridlink-api") {
const key = parseDebridLinkApiKeys(settings.debridLinkApiKeys).find((entry) => entry.id === request.accountId);
if (!key?.token) storedSecretMissing();
return key.token;
}
const provider = singleProvider(request.kind);
if (request.accountId !== `svc-${provider}` || !singleConfigured(settings, request.kind)) storedSecretMissing();
if (request.kind === "bestdebrid-api" && settings.bestToken) return settings.bestToken;
if (request.kind === "alldebrid-api" && settings.allDebridToken) return settings.allDebridToken;
if (request.kind === "deepbrid-api" && settings.deepbridApiKey) return settings.deepbridApiKey;
if (request.kind === "ddownload-login" && settings.ddownloadPassword) return settings.ddownloadPassword;
if (request.kind === "onefichier-api" && settings.oneFichierApiKey) return settings.oneFichierApiKey;
if (request.kind === "linksnappy-login" && settings.linkSnappyPassword) return settings.linkSnappyPassword;
return storedSecretMissing();
}
const REAL_DEBRID_LEGACY_ID = "svc-realdebrid";
function syncRealDebridLegacyFields(settings: AppSettings): AppSettings {
const firstApi = parseRealDebridApiAccounts(settings.realDebridApiTokens)[0];
return {
...settings,
token: firstApi?.token || "",
realDebridUseWebLogin: settings.realDebridWebAccountIds.length > 0
};
}
function normalizeRealDebridCommandSettings(settings: AppSettings): AppSettings {
const rawPool = settings.realDebridApiTokens.trim() || (settings.realDebridUseWebLogin ? "" : settings.token.trim());
const apiAccounts = parseRealDebridApiAccounts(rawPool).map((account) => ({
id: account.id.startsWith("rda_legacy_") ? `rda_${randomUUID().replace(/-/g, "")}` : account.id,
token: account.token
}));
return {
...settings,
realDebridApiTokens: serializeRealDebridApiAccounts(apiAccounts),
realDebridWebAccountIds: settings.realDebridWebAccountIds.length > 0
? [...settings.realDebridWebAccountIds]
: settings.realDebridUseWebLogin ? ["rdw_legacy"] : []
};
}
export function setRealDebridAccountEnabled(settings: AppSettings, accountId: string, enabled: boolean): AppSettings {
const normalized = normalizeRealDebridCommandSettings(settings);
if (!getRealDebridAccounts(normalized).some((account) => account.id === accountId)) invalid();
return {
...normalized,
realDebridDisabledAccountIds: enabled
? normalized.realDebridDisabledAccountIds.filter((id) => id !== accountId)
: [...new Set([...normalized.realDebridDisabledAccountIds, accountId])]
};
}
function migrateRealDebridMetadata(settings: AppSettings, oldId: string, newId: string, limit: number | undefined): AppSettings {
const idChanged = oldId !== newId;
return {
...settings,
realDebridDisabledAccountIds: idChanged
? migrateIdList(settings.realDebridDisabledAccountIds, oldId, newId)
: [...settings.realDebridDisabledAccountIds],
realDebridAccountDailyLimitBytes: migrateLimit(settings.realDebridAccountDailyLimitBytes, oldId, newId, limit),
realDebridAccountDailyUsageBytes: idChanged ? withoutKeys(settings.realDebridAccountDailyUsageBytes, oldId, newId) : { ...settings.realDebridAccountDailyUsageBytes },
realDebridAccountTotalUsageBytes: idChanged ? withoutKeys(settings.realDebridAccountTotalUsageBytes, oldId, newId) : { ...settings.realDebridAccountTotalUsageBytes },
debridAccountStatuses: idChanged ? withoutKeys(settings.debridAccountStatuses, oldId, newId) : { ...settings.debridAccountStatuses }
};
}
function createRealDebrid(settings: AppSettings, command: Extract<AccountCommand, { action: "create" }>): AppliedAccountCommand {
if (command.kind === "realdebrid-api") {
const token = validateSecret(command.secret || "");
const accounts = parseRealDebridApiAccounts(settings.realDebridApiTokens);
if (accounts.some((account) => account.token === token)) invalid();
const accountId = `rda_${randomUUID().replace(/-/g, "")}`;
const next = syncRealDebridLegacyFields({
...settings,
realDebridApiTokens: serializeRealDebridApiAccounts([...accounts, { id: accountId, token }]),
realDebridDisabledAccountIds: settings.realDebridDisabledAccountIds.filter((id) => id !== accountId),
realDebridAccountDailyLimitBytes: setLimit(settings.realDebridAccountDailyLimitBytes, accountId, command.dailyLimitBytes)
});
return { settings: next, response: { accountId } };
}
const requestedId = String(command.identity || "").trim();
const accountId = requestedId && isRealDebridWebAccountId(requestedId) ? requestedId : `rdw_${randomUUID().replace(/-/g, "")}`;
if (settings.realDebridWebAccountIds.includes(accountId)) invalid();
const next = syncRealDebridLegacyFields({
...settings,
realDebridWebAccountIds: [...settings.realDebridWebAccountIds, accountId],
realDebridDisabledAccountIds: settings.realDebridDisabledAccountIds.filter((id) => id !== accountId),
realDebridAccountDailyLimitBytes: setLimit(settings.realDebridAccountDailyLimitBytes, accountId, command.dailyLimitBytes)
});
return { settings: next, response: { accountId } };
}
function replaceRealDebrid(settings: AppSettings, command: Extract<AccountCommand, { action: "replace" }>): AppliedAccountCommand {
const accounts = getRealDebridAccounts(settings);
const current = accounts.find((account) => account.id === command.accountId);
if (!current || (current.kind === "api") !== (command.kind === "realdebrid-api")) invalid();
if (current.kind === "web") {
const next = syncRealDebridLegacyFields(migrateRealDebridMetadata(settings, current.id, current.id, command.dailyLimitBytes));
return { settings: next, response: { accountId: current.id } };
}
const token = command.secret?.trim() ? validateSecret(command.secret) : current.token;
if (accounts.some((account) => account.id !== current.id && account.kind === "api" && account.token === token)) invalid();
const credentials = parseRealDebridApiAccounts(settings.realDebridApiTokens).map((account) => ({
id: account.id,
token: account.id === current.id ? token : account.token
}));
const next = syncRealDebridLegacyFields(migrateRealDebridMetadata({
...settings,
realDebridApiTokens: serializeRealDebridApiAccounts(credentials)
}, current.id, current.id, command.dailyLimitBytes));
return { settings: next, response: { accountId: current.id } };
}
function deleteRealDebrid(settings: AppSettings, command: Extract<AccountCommand, { action: "delete" }>): AppliedAccountCommand {
const account = getRealDebridAccounts(settings).find((entry) => entry.id === command.accountId);
if (!account || (account.kind === "api") !== (command.kind === "realdebrid-api")) invalid();
const next = syncRealDebridLegacyFields({
...settings,
realDebridApiTokens: account.kind === "api"
? serializeRealDebridApiAccounts(parseRealDebridApiAccounts(settings.realDebridApiTokens).filter((entry) => entry.id !== account.id))
: settings.realDebridApiTokens,
realDebridWebAccountIds: account.kind === "web" ? settings.realDebridWebAccountIds.filter((id) => id !== account.id) : [...settings.realDebridWebAccountIds],
realDebridDisabledAccountIds: settings.realDebridDisabledAccountIds.filter((id) => id !== account.id),
realDebridAccountDailyLimitBytes: withoutKeys(settings.realDebridAccountDailyLimitBytes, account.id),
realDebridAccountDailyUsageBytes: withoutKeys(settings.realDebridAccountDailyUsageBytes, account.id),
realDebridAccountTotalUsageBytes: withoutKeys(settings.realDebridAccountTotalUsageBytes, account.id),
debridAccountStatuses: withoutKeys(settings.debridAccountStatuses, account.id)
});
return { settings: next, response: { accountId: getRealDebridAccounts(next)[0]?.id || null } };
}
function withoutKeys<T>(record: Record<string, T>, ...keys: string[]): Record<string, T> {
const removed = new Set(keys);
return Object.fromEntries(Object.entries(record).filter(([key]) => !removed.has(key)));
}
function migrateIdList(values: readonly string[], oldId: string, newId: string): string[] {
const retained = values.filter((value) => value !== oldId && value !== newId);
if (values.includes(oldId)) {
retained.push(newId);
}
return retained;
}
function setLimit(record: Record<string, number>, id: string, value: number | undefined): Record<string, number> {
const result = { ...record };
if (value === undefined) {
return result;
}
if (value > 0) {
result[id] = value;
} else {
delete result[id];
}
return result;
}
function migrateLimit(record: Record<string, number>, oldId: string, newId: string, value: number | undefined): Record<string, number> {
const result = withoutKeys(record, oldId, newId);
const resolved = value === undefined ? record[oldId] : value;
if (resolved && resolved > 0) {
result[newId] = resolved;
}
return result;
}
function validateIdentity(identity: string): string {
const trimmed = identity.trim();
if (!trimmed || /[:\r\n]/.test(trimmed)) {
invalid();
}
return trimmed;
}
function validateSecret(secret: string): string {
if (!secret.trim() || /[\r\n]/.test(secret)) {
invalid();
}
return secret;
}
function megaMode(kind: RendererAccountKind): MegaDebridAccountMode {
return kind === "megadebrid-web" ? "web" : "api";
}
function writeMegaPools(settings: AppSettings, apiCredentials: string, webCredentials: string): AppSettings {
const mergedCredentials = mergeMegaDebridCredentialPools(apiCredentials, webCredentials);
const first = parseMegaDebridAccounts(mergedCredentials)[0];
return {
...settings,
megaCredentials: mergedCredentials,
megaLogin: first?.login || "",
megaPassword: first?.password || "",
megaDebridApiCredentials: apiCredentials,
megaDebridWebCredentials: webCredentials,
megaDebridApiEnabled: settings.megaDebridApiEnabled && Boolean(apiCredentials),
megaDebridWebEnabled: settings.megaDebridWebEnabled && Boolean(webCredentials)
};
}
function createMega(settings: AppSettings, command: Extract<AccountCommand, { action: "create" }>): AppliedAccountCommand {
const mode = megaMode(command.kind);
const identity = validateIdentity(command.identity || "");
const secret = validateSecret(command.secret || "");
const accounts = getMegaDebridAccountsForMode(settings, mode);
if (accounts.some((account) => account.login.toLowerCase() === identity.toLowerCase())) {
invalid();
}
const selectedCredentials = serializeMegaDebridAccounts([...accounts, { login: identity, password: secret }]);
const apiCredentials = mode === "api" ? selectedCredentials : getMegaDebridCredentialsForMode(settings, "api");
const webCredentials = mode === "web" ? selectedCredentials : getMegaDebridCredentialsForMode(settings, "web");
const accountId = getMegaDebridAccountId(identity);
const next = writeMegaPools(settings, apiCredentials, webCredentials);
if (mode === "api") {
next.megaDebridApiEnabled = true;
} else {
next.megaDebridWebEnabled = true;
}
next.megaDebridAccountDailyLimitBytes = setLimit(settings.megaDebridAccountDailyLimitBytes, accountId, command.dailyLimitBytes);
return { settings: next, response: { accountId } };
}
function replaceMega(settings: AppSettings, command: Extract<AccountCommand, { action: "replace" }>): AppliedAccountCommand {
const mode = megaMode(command.kind);
const accounts = getMegaDebridAccountsForMode(settings, mode);
const index = accounts.findIndex((account) => account.id === command.accountId);
if (index < 0) {
invalid();
}
const current = accounts[index];
const identity = command.identity === undefined ? current.login : validateIdentity(command.identity);
const secret = command.secret?.trim() ? validateSecret(command.secret) : current.password;
if (accounts.some((account, accountIndex) => accountIndex !== index && account.login.toLowerCase() === identity.toLowerCase())) {
invalid();
}
const accountId = getMegaDebridAccountId(identity);
const selectedCredentials = serializeMegaDebridAccounts(accounts.map((account, accountIndex) => accountIndex === index
? { login: identity, password: secret }
: { login: account.login, password: account.password }));
const apiCredentials = mode === "api" ? selectedCredentials : getMegaDebridCredentialsForMode(settings, "api");
const webCredentials = mode === "web" ? selectedCredentials : getMegaDebridCredentialsForMode(settings, "web");
const next = writeMegaPools(settings, apiCredentials, webCredentials);
const selectedDisabled = migrateIdList(getMegaDebridDisabledAccountIdsForMode(settings, mode), command.accountId, accountId);
if (mode === "api") {
next.megaDebridApiDisabledAccountIds = selectedDisabled;
} else {
next.megaDebridWebDisabledAccountIds = selectedDisabled;
}
next.megaDebridDisabledAccountIds = [...new Set([...next.megaDebridApiDisabledAccountIds, ...next.megaDebridWebDisabledAccountIds])];
next.megaDebridAccountDailyLimitBytes = migrateLimit(settings.megaDebridAccountDailyLimitBytes, command.accountId, accountId, command.dailyLimitBytes);
if (command.accountId !== accountId) {
next.megaDebridAccountDailyUsageBytes = withoutKeys(settings.megaDebridAccountDailyUsageBytes, command.accountId, accountId);
next.megaDebridAccountTotalUsageBytes = withoutKeys(settings.megaDebridAccountTotalUsageBytes, command.accountId, accountId);
next.debridAccountStatuses = withoutKeys(settings.debridAccountStatuses, command.accountId, accountId);
}
return { settings: next, response: { accountId } };
}
function deleteMega(settings: AppSettings, command: Extract<AccountCommand, { action: "delete" }>): AppliedAccountCommand {
const mode = megaMode(command.kind);
const selected = getMegaDebridAccountsForMode(settings, mode);
if (!selected.some((account) => account.id === command.accountId)) {
invalid();
}
const selectedCredentials = serializeMegaDebridAccounts(selected.filter((account) => account.id !== command.accountId));
const apiCredentials = mode === "api" ? selectedCredentials : getMegaDebridCredentialsForMode(settings, "api");
const webCredentials = mode === "web" ? selectedCredentials : getMegaDebridCredentialsForMode(settings, "web");
const accountStillUsed = parseMegaDebridAccounts(mode === "api" ? webCredentials : apiCredentials).some((account) => account.id === command.accountId);
const next = writeMegaPools(settings, apiCredentials, webCredentials);
next.megaDebridApiDisabledAccountIds = mode === "api"
? settings.megaDebridApiDisabledAccountIds.filter((id) => id !== command.accountId)
: [...settings.megaDebridApiDisabledAccountIds];
next.megaDebridWebDisabledAccountIds = mode === "web"
? settings.megaDebridWebDisabledAccountIds.filter((id) => id !== command.accountId)
: [...settings.megaDebridWebDisabledAccountIds];
next.megaDebridDisabledAccountIds = [...new Set([...next.megaDebridApiDisabledAccountIds, ...next.megaDebridWebDisabledAccountIds])];
if (!accountStillUsed) {
next.megaDebridAccountDailyLimitBytes = withoutKeys(settings.megaDebridAccountDailyLimitBytes, command.accountId);
next.megaDebridAccountDailyUsageBytes = withoutKeys(settings.megaDebridAccountDailyUsageBytes, command.accountId);
next.megaDebridAccountTotalUsageBytes = withoutKeys(settings.megaDebridAccountTotalUsageBytes, command.accountId);
next.debridAccountStatuses = withoutKeys(settings.debridAccountStatuses, command.accountId);
}
const remaining = getMegaDebridAccountsForMode(next, mode)[0]?.id || null;
return { settings: next, response: { accountId: remaining } };
}
function createDebridLink(settings: AppSettings, command: Extract<AccountCommand, { action: "create" }>): AppliedAccountCommand {
const secret = validateSecret(command.secret || "");
if (/[,\r\n]/.test(secret)) {
invalid();
}
const keys = parseDebridLinkApiKeys(settings.debridLinkApiKeys);
if (keys.some((key) => key.token === secret)) {
invalid();
}
const accountId = getDebridLinkApiKeyId(secret);
return {
settings: {
...settings,
debridLinkApiKeys: [...keys.map((key) => key.token), secret].join("\n"),
debridLinkApiKeyDailyLimitBytes: setLimit(settings.debridLinkApiKeyDailyLimitBytes, accountId, command.dailyLimitBytes)
},
response: { accountId }
};
}
function replaceDebridLink(settings: AppSettings, command: Extract<AccountCommand, { action: "replace" }>): AppliedAccountCommand {
const keys = parseDebridLinkApiKeys(settings.debridLinkApiKeys);
const index = keys.findIndex((key) => key.id === command.accountId);
if (index < 0) {
invalid();
}
const secret = command.secret?.trim() ? validateSecret(command.secret) : keys[index].token;
if (/[,\r\n]/.test(secret) || keys.some((key, keyIndex) => keyIndex !== index && key.token === secret)) {
invalid();
}
const accountId = getDebridLinkApiKeyId(secret);
const tokens = keys.map((key, keyIndex) => keyIndex === index ? secret : key.token);
const idChanged = accountId !== command.accountId;
return {
settings: {
...settings,
debridLinkApiKeys: tokens.join("\n"),
debridLinkDisabledKeyIds: idChanged ? migrateIdList(settings.debridLinkDisabledKeyIds, command.accountId, accountId) : [...settings.debridLinkDisabledKeyIds],
debridLinkApiKeyDailyLimitBytes: migrateLimit(settings.debridLinkApiKeyDailyLimitBytes, command.accountId, accountId, command.dailyLimitBytes),
debridLinkApiKeyDailyUsageBytes: idChanged ? withoutKeys(settings.debridLinkApiKeyDailyUsageBytes, command.accountId, accountId) : { ...settings.debridLinkApiKeyDailyUsageBytes },
debridLinkApiKeyTotalUsageBytes: idChanged ? withoutKeys(settings.debridLinkApiKeyTotalUsageBytes, command.accountId, accountId) : { ...settings.debridLinkApiKeyTotalUsageBytes },
debridAccountStatuses: idChanged ? withoutKeys(settings.debridAccountStatuses, command.accountId, accountId) : { ...settings.debridAccountStatuses }
},
response: { accountId }
};
}
function deleteDebridLink(settings: AppSettings, command: Extract<AccountCommand, { action: "delete" }>): AppliedAccountCommand {
const keys = parseDebridLinkApiKeys(settings.debridLinkApiKeys);
if (!keys.some((key) => key.id === command.accountId)) {
invalid();
}
const remaining = keys.filter((key) => key.id !== command.accountId);
return {
settings: {
...settings,
debridLinkApiKeys: remaining.map((key) => key.token).join("\n"),
debridLinkDisabledKeyIds: settings.debridLinkDisabledKeyIds.filter((id) => id !== command.accountId),
debridLinkApiKeyDailyLimitBytes: withoutKeys(settings.debridLinkApiKeyDailyLimitBytes, command.accountId),
debridLinkApiKeyDailyUsageBytes: withoutKeys(settings.debridLinkApiKeyDailyUsageBytes, command.accountId),
debridLinkApiKeyTotalUsageBytes: withoutKeys(settings.debridLinkApiKeyTotalUsageBytes, command.accountId),
debridAccountStatuses: withoutKeys(settings.debridAccountStatuses, command.accountId)
},
response: { accountId: remaining[0]?.id || null }
};
}
function singleProvider(kind: RendererAccountKind): DebridProvider {
if (kind.startsWith("realdebrid")) return "realdebrid";
if (kind.startsWith("bestdebrid")) return "bestdebrid";
if (kind.startsWith("alldebrid")) return "alldebrid";
if (kind === "deepbrid-api") return "deepbrid";
if (kind === "ddownload-login") return "ddownload";
if (kind === "onefichier-api") return "onefichier";
if (kind === "linksnappy-login") return "linksnappy";
invalid();
}
function singleConfigured(settings: AppSettings, kind: RendererAccountKind): boolean {
if (kind === "realdebrid-api") return Boolean(settings.token.trim());
if (kind === "realdebrid-web") return settings.realDebridUseWebLogin;
if (kind === "bestdebrid-api") return Boolean(settings.bestToken.trim());
if (kind === "bestdebrid-web") return settings.bestDebridUseWebLogin;
if (kind === "alldebrid-api") return Boolean(settings.allDebridToken.trim());
if (kind === "alldebrid-web") return settings.allDebridUseWebLogin;
if (kind === "deepbrid-api") return Boolean(settings.deepbridApiKey.trim());
if (kind === "ddownload-login") return Boolean(settings.ddownloadLogin.trim() && settings.ddownloadPassword);
if (kind === "onefichier-api") return Boolean(settings.oneFichierApiKey.trim());
if (kind === "linksnappy-login") return Boolean(settings.linkSnappyLogin.trim() && settings.linkSnappyPassword);
return false;
}
function setSingle(settings: AppSettings, kind: RendererAccountKind, identity: string | undefined, secret: string | undefined): AppSettings {
if (kind === "realdebrid-api") return { ...settings, token: validateSecret(secret || ""), realDebridUseWebLogin: false };
if (kind === "realdebrid-web") return { ...settings, token: "", realDebridUseWebLogin: true };
if (kind === "bestdebrid-api") return { ...settings, bestToken: validateSecret(secret || ""), bestDebridUseWebLogin: false };
if (kind === "bestdebrid-web") return { ...settings, bestToken: "", bestDebridUseWebLogin: true };
if (kind === "alldebrid-api") return { ...settings, allDebridToken: validateSecret(secret || ""), allDebridUseWebLogin: false };
if (kind === "alldebrid-web") return { ...settings, allDebridToken: "", allDebridUseWebLogin: true };
if (kind === "deepbrid-api") return { ...settings, deepbridApiKey: validateSecret(secret || "") };
if (kind === "ddownload-login") return { ...settings, ddownloadLogin: validateIdentity(identity || ""), ddownloadPassword: validateSecret(secret || "") };
if (kind === "onefichier-api") return { ...settings, oneFichierApiKey: validateSecret(secret || "") };
if (kind === "linksnappy-login") return { ...settings, linkSnappyLogin: validateIdentity(identity || ""), linkSnappyPassword: validateSecret(secret || "") };
invalid();
}
function replaceSingle(settings: AppSettings, command: Extract<AccountCommand, { action: "replace" }>): AppliedAccountCommand {
if (!singleConfigured(settings, command.kind) || command.accountId !== `svc-${singleProvider(command.kind)}`) {
invalid();
}
let identity = command.identity;
let secret = command.secret?.trim() ? command.secret : undefined;
if (command.kind === "realdebrid-api") secret ||= settings.token;
if (command.kind === "bestdebrid-api") secret ||= settings.bestToken;
if (command.kind === "alldebrid-api") secret ||= settings.allDebridToken;
if (command.kind === "deepbrid-api") secret ||= settings.deepbridApiKey;
if (command.kind === "ddownload-login") {
identity = identity?.trim() ? identity : settings.ddownloadLogin;
secret ||= settings.ddownloadPassword;
}
if (command.kind === "onefichier-api") secret ||= settings.oneFichierApiKey;
if (command.kind === "linksnappy-login") {
identity = identity?.trim() ? identity : settings.linkSnappyLogin;
secret ||= settings.linkSnappyPassword;
}
const provider = singleProvider(command.kind);
const next = setSingle(settings, command.kind, identity, secret);
next.providerDailyLimitBytes = setLimit(settings.providerDailyLimitBytes as Record<string, number>, provider, command.dailyLimitBytes);
return { settings: next, response: { accountId: command.accountId } };
}
function deleteSingle(settings: AppSettings, command: Extract<AccountCommand, { action: "delete" }>): AppliedAccountCommand {
const provider = singleProvider(command.kind);
if (!singleConfigured(settings, command.kind) || command.accountId !== `svc-${provider}`) {
invalid();
}
let next = { ...settings };
if (provider === "realdebrid") next = { ...next, token: "", realDebridUseWebLogin: false };
if (provider === "bestdebrid") next = { ...next, bestToken: "", bestDebridUseWebLogin: false };
if (provider === "alldebrid") next = { ...next, allDebridToken: "", allDebridUseWebLogin: false };
if (provider === "deepbrid") next = { ...next, deepbridApiKey: "" };
if (provider === "ddownload") next = { ...next, ddownloadLogin: "", ddownloadPassword: "" };
if (provider === "onefichier") next = { ...next, oneFichierApiKey: "" };
if (provider === "linksnappy") next = { ...next, linkSnappyLogin: "", linkSnappyPassword: "" };
next.providerDailyLimitBytes = withoutKeys(settings.providerDailyLimitBytes as Record<string, number>, provider);
next.providerDailyUsageBytes = withoutKeys(settings.providerDailyUsageBytes as Record<string, number>, provider);
next.providerTotalUsageBytes = withoutKeys(settings.providerTotalUsageBytes as Record<string, number>, provider);
next.debridAccountStatuses = withoutKeys(settings.debridAccountStatuses, command.accountId);
return { settings: next, response: { accountId: null } };
}
function createSingle(settings: AppSettings, command: Extract<AccountCommand, { action: "create" }>): AppliedAccountCommand {
if (singleConfigured(settings, command.kind)) {
invalid();
}
const provider = singleProvider(command.kind);
const next = setSingle(settings, command.kind, command.identity, command.secret);
next.providerDailyLimitBytes = setLimit(settings.providerDailyLimitBytes as Record<string, number>, provider, command.dailyLimitBytes);
return { settings: next, response: { accountId: `svc-${provider}` } };
}
export function applyAccountCommand(settings: AppSettings, command: AccountCommand): AppliedAccountCommand {
if (command.action === "update-secret") {
const replace: Extract<AccountCommand, { action: "replace" }> = {
action: "replace",
kind: command.kind,
accountId: command.accountId,
secret: command.secret
};
return applyAccountCommand(settings, replace);
}
if (command.kind === "realdebrid-api" || command.kind === "realdebrid-web") {
const normalizedSettings = normalizeRealDebridCommandSettings(settings);
if (command.action === "create") return createRealDebrid(normalizedSettings, command);
if (command.action === "replace") return replaceRealDebrid(normalizedSettings, command);
return deleteRealDebrid(normalizedSettings, command);
}
if (command.kind === "megadebrid-api" || command.kind === "megadebrid-web") {
if (command.action === "create") return createMega(settings, command);
if (command.action === "replace") return replaceMega(settings, command);
return deleteMega(settings, command);
}
if (command.kind === "debridlink-api") {
if (command.action === "create") return createDebridLink(settings, command);
if (command.action === "replace") return replaceDebridLink(settings, command);
return deleteDebridLink(settings, command);
}
if (command.action === "create") return createSingle(settings, command);
if (command.action === "replace") return replaceSingle(settings, command);
return deleteSingle(settings, command);
}