feat(accounts): manage Real-Debrid account pool
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { getDebridLinkApiKeyId, parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import {
|
||||
getMegaDebridAccountId,
|
||||
getMegaDebridAccountsForMode,
|
||||
@@ -9,6 +10,7 @@ import {
|
||||
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 {
|
||||
@@ -150,6 +152,13 @@ function storedSecretMissing(): never {
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -163,7 +172,6 @@ export function resolveStoredAccountSecret(settings: AppSettings, request: Accou
|
||||
}
|
||||
const provider = singleProvider(request.kind);
|
||||
if (request.accountId !== `svc-${provider}` || !singleConfigured(settings, request.kind)) storedSecretMissing();
|
||||
if (request.kind === "realdebrid-api" && settings.token) return settings.token;
|
||||
if (request.kind === "bestdebrid-api" && settings.bestToken) return settings.bestToken;
|
||||
if (request.kind === "alldebrid-api" && settings.allDebridToken) return settings.allDebridToken;
|
||||
if (request.kind === "ddownload-login" && settings.ddownloadPassword) return settings.ddownloadPassword;
|
||||
@@ -172,6 +180,122 @@ export function resolveStoredAccountSecret(settings: AppSettings, request: Accou
|
||||
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)));
|
||||
@@ -492,6 +616,12 @@ export function applyAccountCommand(settings: AppSettings, command: AccountComma
|
||||
};
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user