feat(accounts): manage Real-Debrid account pool
This commit is contained in:
+54
-12
@@ -1,6 +1,7 @@
|
|||||||
import type { AccountCheckScope, AppSettings, DebridAccountStatus, DebridProvider } from "../shared/types";
|
import type { AccountCheckScope, AppSettings, DebridAccountStatus, DebridProvider } from "../shared/types";
|
||||||
import { getMegaDebridAccountsForMode, getMegaDebridDisabledAccountIdsForMode, parseMegaDebridAccounts, type MegaDebridAccountEntry } from "../shared/mega-debrid-accounts";
|
import { getMegaDebridAccountsForMode, getMegaDebridDisabledAccountIdsForMode, parseMegaDebridAccounts, type MegaDebridAccountEntry } from "../shared/mega-debrid-accounts";
|
||||||
import { parseDebridLinkApiKeys, type DebridLinkApiKeyEntry } from "../shared/debrid-link-keys";
|
import { parseDebridLinkApiKeys, type DebridLinkApiKeyEntry } from "../shared/debrid-link-keys";
|
||||||
|
import { getRealDebridAccounts, type RealDebridAccountEntry } from "../shared/real-debrid-accounts";
|
||||||
import { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
import { compactErrorText } from "./utils";
|
import { compactErrorText } from "./utils";
|
||||||
|
|
||||||
@@ -23,6 +24,15 @@ export interface RealDebridSessionProbeResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type RealDebridSessionProbe = (signal?: AbortSignal) => Promise<RealDebridSessionProbeResult>;
|
export type RealDebridSessionProbe = (signal?: AbortSignal) => Promise<RealDebridSessionProbeResult>;
|
||||||
|
export type RealDebridSessionProbeResolver = (accountId: string, signal?: AbortSignal) => Promise<RealDebridSessionProbeResult>;
|
||||||
|
|
||||||
|
export function retainConfiguredRealDebridStatuses(settings: AppSettings, statuses: readonly DebridAccountStatus[]): DebridAccountStatus[] {
|
||||||
|
const accountIds = new Set(getRealDebridAccounts(settings).map((account) => account.id));
|
||||||
|
if (accountIds.size === 0 && (settings.realDebridUseWebLogin || settings.token.trim())) {
|
||||||
|
accountIds.add(REAL_DEBRID_STATUS_ID);
|
||||||
|
}
|
||||||
|
return statuses.filter((status) => status.provider !== "realdebrid" || accountIds.has(status.accountId));
|
||||||
|
}
|
||||||
|
|
||||||
function timeoutSignal(signal: AbortSignal | undefined, ms: number): AbortSignal {
|
function timeoutSignal(signal: AbortSignal | undefined, ms: number): AbortSignal {
|
||||||
const timeout = AbortSignal.timeout(ms);
|
const timeout = AbortSignal.timeout(ms);
|
||||||
@@ -69,18 +79,30 @@ function maskSecret(value: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function checkRealDebridAccount(
|
export async function checkRealDebridAccount(
|
||||||
settings: AppSettings,
|
accountOrSettings: RealDebridAccountEntry | AppSettings,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
now = Date.now(),
|
now = Date.now(),
|
||||||
probeWebSession?: RealDebridSessionProbe
|
probeWebSession?: RealDebridSessionProbe
|
||||||
): Promise<DebridAccountStatus> {
|
): Promise<DebridAccountStatus> {
|
||||||
const token = String(settings.token || "").trim();
|
const isAccount = typeof (accountOrSettings as RealDebridAccountEntry).kind === "string";
|
||||||
const useWebLogin = Boolean(settings.realDebridUseWebLogin);
|
const account = isAccount
|
||||||
|
? accountOrSettings as RealDebridAccountEntry
|
||||||
|
: {
|
||||||
|
id: REAL_DEBRID_STATUS_ID,
|
||||||
|
kind: (accountOrSettings as AppSettings).realDebridUseWebLogin ? "web" as const : "api" as const,
|
||||||
|
index: 0,
|
||||||
|
label: "Real-Debrid",
|
||||||
|
maskedLogin: (accountOrSettings as AppSettings).realDebridUseWebLogin ? "Browser-Login" : maskSecret(String((accountOrSettings as AppSettings).token || "")),
|
||||||
|
enabled: true,
|
||||||
|
...((accountOrSettings as AppSettings).realDebridUseWebLogin ? {} : { token: String((accountOrSettings as AppSettings).token || "").trim() })
|
||||||
|
} as RealDebridAccountEntry;
|
||||||
|
const token = account.kind === "api" ? account.token.trim() : "";
|
||||||
|
const useWebLogin = account.kind === "web";
|
||||||
const base: DebridAccountStatus = {
|
const base: DebridAccountStatus = {
|
||||||
accountId: REAL_DEBRID_STATUS_ID,
|
accountId: account.id,
|
||||||
provider: "realdebrid",
|
provider: "realdebrid",
|
||||||
label: "Real-Debrid",
|
label: account.label,
|
||||||
maskedLogin: useWebLogin ? "Browser-Login" : maskSecret(token),
|
maskedLogin: account.maskedLogin,
|
||||||
valid: false,
|
valid: false,
|
||||||
isPremium: false,
|
isPremium: false,
|
||||||
premiumUntilMs: null,
|
premiumUntilMs: null,
|
||||||
@@ -274,7 +296,7 @@ export async function checkDebridLinkKey(
|
|||||||
export async function checkAllDebridAccounts(
|
export async function checkAllDebridAccounts(
|
||||||
settings: AppSettings,
|
settings: AppSettings,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
probeRealDebridWebSession?: RealDebridSessionProbe,
|
probeRealDebridWebSession?: RealDebridSessionProbeResolver,
|
||||||
scope: AccountCheckScope = "all"
|
scope: AccountCheckScope = "all"
|
||||||
): Promise<DebridAccountStatus[]> {
|
): Promise<DebridAccountStatus[]> {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -298,13 +320,33 @@ export async function checkAllDebridAccounts(
|
|||||||
: providerEnabled("debridlink")
|
: providerEnabled("debridlink")
|
||||||
? configuredDebridLinkKeys.filter((key) => !(settings.debridLinkDisabledKeyIds || []).includes(key.id))
|
? configuredDebridLinkKeys.filter((key) => !(settings.debridLinkDisabledKeyIds || []).includes(key.id))
|
||||||
: [];
|
: [];
|
||||||
const checkRealDebrid = Boolean(settings.realDebridUseWebLogin || String(settings.token || "").trim())
|
const configuredRealDebridAccounts = getRealDebridAccounts(settings);
|
||||||
&& (scope === "all" || providerEnabled("realdebrid"));
|
const legacyRealDebridAccounts: RealDebridAccountEntry[] = configuredRealDebridAccounts.length === 0
|
||||||
|
&& (settings.realDebridUseWebLogin || String(settings.token || "").trim())
|
||||||
|
? [{
|
||||||
|
id: REAL_DEBRID_STATUS_ID,
|
||||||
|
kind: settings.realDebridUseWebLogin ? "web" : "api",
|
||||||
|
index: 0,
|
||||||
|
label: "Real-Debrid",
|
||||||
|
maskedLogin: settings.realDebridUseWebLogin ? "Browser-Login" : maskSecret(settings.token),
|
||||||
|
enabled: true,
|
||||||
|
...(settings.realDebridUseWebLogin ? {} : { token: settings.token.trim() })
|
||||||
|
} as RealDebridAccountEntry]
|
||||||
|
: [];
|
||||||
|
const allRealDebridAccounts = configuredRealDebridAccounts.length > 0 ? configuredRealDebridAccounts : legacyRealDebridAccounts;
|
||||||
|
const realDebridAccounts = scope === "all"
|
||||||
|
? allRealDebridAccounts
|
||||||
|
: providerEnabled("realdebrid") ? allRealDebridAccounts.filter((account) => account.enabled) : [];
|
||||||
|
|
||||||
const taskFns: Array<() => Promise<DebridAccountStatus>> = [
|
const taskFns: Array<() => Promise<DebridAccountStatus>> = [
|
||||||
...(checkRealDebrid
|
...realDebridAccounts.map((account) => () => checkRealDebridAccount(
|
||||||
? [() => checkRealDebridAccount(settings, signal, now, probeRealDebridWebSession)]
|
account,
|
||||||
: []),
|
signal,
|
||||||
|
now,
|
||||||
|
account.kind === "web" && probeRealDebridWebSession
|
||||||
|
? (probeSignal) => probeRealDebridWebSession(account.id, probeSignal)
|
||||||
|
: undefined
|
||||||
|
)),
|
||||||
...megaAccounts.map((account) => () => checkMegaDebridAccount(account, signal, now)),
|
...megaAccounts.map((account) => () => checkMegaDebridAccount(account, signal, now)),
|
||||||
...debridLinkKeys.map((key) => () => checkDebridLinkKey(key, signal, now))
|
...debridLinkKeys.map((key) => () => checkDebridLinkKey(key, signal, now))
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { getDebridLinkApiKeyId, parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
import { getDebridLinkApiKeyId, parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
import {
|
import {
|
||||||
getMegaDebridAccountId,
|
getMegaDebridAccountId,
|
||||||
getMegaDebridAccountsForMode,
|
getMegaDebridAccountsForMode,
|
||||||
@@ -9,6 +10,7 @@ import {
|
|||||||
serializeMegaDebridAccounts,
|
serializeMegaDebridAccounts,
|
||||||
type MegaDebridAccountMode
|
type MegaDebridAccountMode
|
||||||
} from "../shared/mega-debrid-accounts";
|
} 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";
|
import type { AccountCommand, AccountCredentialCheckInput, AccountSecretRequest, AppSettings, DebridProvider, RendererAccountKind } from "../shared/types";
|
||||||
|
|
||||||
export interface AppliedAccountCommand {
|
export interface AppliedAccountCommand {
|
||||||
@@ -150,6 +152,13 @@ function storedSecretMissing(): never {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function resolveStoredAccountSecret(settings: AppSettings, request: AccountSecretRequest): string {
|
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") {
|
if (request.kind === "megadebrid-api" || request.kind === "megadebrid-web") {
|
||||||
const mode = request.kind === "megadebrid-web" ? "web" : "api";
|
const mode = request.kind === "megadebrid-web" ? "web" : "api";
|
||||||
const account = getMegaDebridAccountsForMode(settings, mode).find((entry) => entry.id === request.accountId);
|
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);
|
const provider = singleProvider(request.kind);
|
||||||
if (request.accountId !== `svc-${provider}` || !singleConfigured(settings, request.kind)) storedSecretMissing();
|
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 === "bestdebrid-api" && settings.bestToken) return settings.bestToken;
|
||||||
if (request.kind === "alldebrid-api" && settings.allDebridToken) return settings.allDebridToken;
|
if (request.kind === "alldebrid-api" && settings.allDebridToken) return settings.allDebridToken;
|
||||||
if (request.kind === "ddownload-login" && settings.ddownloadPassword) return settings.ddownloadPassword;
|
if (request.kind === "ddownload-login" && settings.ddownloadPassword) return settings.ddownloadPassword;
|
||||||
@@ -172,6 +180,122 @@ export function resolveStoredAccountSecret(settings: AppSettings, request: Accou
|
|||||||
return storedSecretMissing();
|
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> {
|
function withoutKeys<T>(record: Record<string, T>, ...keys: string[]): Record<string, T> {
|
||||||
const removed = new Set(keys);
|
const removed = new Set(keys);
|
||||||
return Object.fromEntries(Object.entries(record).filter(([key]) => !removed.has(key)));
|
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);
|
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.kind === "megadebrid-api" || command.kind === "megadebrid-web") {
|
||||||
if (command.action === "create") return createMega(settings, command);
|
if (command.action === "create") return createMega(settings, command);
|
||||||
if (command.action === "replace") return replaceMega(settings, command);
|
if (command.action === "replace") return replaceMega(settings, command);
|
||||||
|
|||||||
+28
-10
@@ -1,6 +1,7 @@
|
|||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import v8 from "node:v8";
|
import v8 from "node:v8";
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import {
|
import {
|
||||||
AddLinksPayload,
|
AddLinksPayload,
|
||||||
@@ -33,10 +34,11 @@ import { importDlcContainers } from "./container";
|
|||||||
import { APP_VERSION, ONLINE_BACKUP_API_URL } from "./constants";
|
import { APP_VERSION, ONLINE_BACKUP_API_URL } from "./constants";
|
||||||
import { DownloadManager } from "./download-manager";
|
import { DownloadManager } from "./download-manager";
|
||||||
import { fetchAllDebridHostInfo, fetchDebridLinkHostLimits } from "./debrid";
|
import { fetchAllDebridHostInfo, fetchDebridLinkHostLimits } from "./debrid";
|
||||||
import { checkAllDebridAccounts, checkDebridLinkKey, checkMegaDebridAccount, checkRealDebridAccount } from "./account-check";
|
import { checkAllDebridAccounts, checkDebridLinkKey, checkMegaDebridAccount, checkRealDebridAccount, retainConfiguredRealDebridStatuses } from "./account-check";
|
||||||
import { parseMegaDebridAccounts } from "../shared/mega-debrid-accounts";
|
import { parseMegaDebridAccounts } from "../shared/mega-debrid-accounts";
|
||||||
import { getMegaDebridAccountsForMode } from "../shared/mega-debrid-accounts";
|
import { getMegaDebridAccountsForMode } from "../shared/mega-debrid-accounts";
|
||||||
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
||||||
|
import { getRealDebridAccounts } from "../shared/real-debrid-accounts";
|
||||||
import { applyAccountCommand, resolveStoredAccountSecret } from "./account-commands";
|
import { applyAccountCommand, resolveStoredAccountSecret } from "./account-commands";
|
||||||
import { collectAccountStatusRedactionValues, sanitizeDebridAccountStatus, sanitizeDebridAccountStatuses } from "./account-status-sanitizer";
|
import { collectAccountStatusRedactionValues, sanitizeDebridAccountStatus, sanitizeDebridAccountStatuses } from "./account-status-sanitizer";
|
||||||
import { createRendererState } from "./renderer-state";
|
import { createRendererState } from "./renderer-state";
|
||||||
@@ -524,6 +526,11 @@ export class AppController {
|
|||||||
if (!key) throw new Error("Account-Payload ist ungültig");
|
if (!key) throw new Error("Account-Payload ist ungültig");
|
||||||
checkedStatus = await checkDebridLinkKey(key);
|
checkedStatus = await checkDebridLinkKey(key);
|
||||||
}
|
}
|
||||||
|
if (command.action !== "delete" && applied.response.accountId && command.kind === "realdebrid-api") {
|
||||||
|
const account = getRealDebridAccounts(applied.settings).find((entry) => entry.id === applied.response.accountId && entry.kind === "api");
|
||||||
|
if (!account) throw new Error("Account-Payload ist ungültig");
|
||||||
|
checkedStatus = await checkRealDebridAccount(account);
|
||||||
|
}
|
||||||
if (checkedStatus) {
|
if (checkedStatus) {
|
||||||
checkedStatus = sanitizeDebridAccountStatus(checkedStatus, redactions);
|
checkedStatus = sanitizeDebridAccountStatus(checkedStatus, redactions);
|
||||||
}
|
}
|
||||||
@@ -547,19 +554,28 @@ export class AppController {
|
|||||||
const redactions = collectAccountStatusRedactionValues(this.settings, input);
|
const redactions = collectAccountStatusRedactionValues(this.settings, input);
|
||||||
if (input.kind === "realdebrid-api" || input.kind === "realdebrid-web") {
|
if (input.kind === "realdebrid-api" || input.kind === "realdebrid-web") {
|
||||||
const useWebLogin = input.kind === "realdebrid-web";
|
const useWebLogin = input.kind === "realdebrid-web";
|
||||||
const settings = input.secret?.trim()
|
const account = input.secret?.trim() && !useWebLogin
|
||||||
? { ...this.settings, token: input.secret.trim(), realDebridUseWebLogin: useWebLogin }
|
? {
|
||||||
: { ...this.settings, realDebridUseWebLogin: useWebLogin };
|
id: `rda_${randomUUID().replace(/-/g, "")}`,
|
||||||
|
kind: "api" as const,
|
||||||
|
index: 0,
|
||||||
|
label: "Real-Debrid",
|
||||||
|
maskedLogin: "Geschützter API-Token",
|
||||||
|
enabled: true,
|
||||||
|
token: input.secret.trim()
|
||||||
|
}
|
||||||
|
: getRealDebridAccounts(this.settings).find((entry) => entry.id === input.accountId && entry.kind === (useWebLogin ? "web" : "api"));
|
||||||
|
if (!account) throw new Error("Account-Payload ist ungültig");
|
||||||
const status = sanitizeDebridAccountStatus(
|
const status = sanitizeDebridAccountStatus(
|
||||||
await checkRealDebridAccount(
|
await checkRealDebridAccount(
|
||||||
settings,
|
account,
|
||||||
undefined,
|
undefined,
|
||||||
Date.now(),
|
Date.now(),
|
||||||
useWebLogin ? (signal) => this.realDebridWebFallback.probeLoginState(signal) : undefined
|
useWebLogin ? (signal) => this.realDebridWebFallback.probeLoginState(signal) : undefined
|
||||||
),
|
),
|
||||||
redactions
|
redactions
|
||||||
);
|
);
|
||||||
if (!input.secret && useWebLogin === this.settings.realDebridUseWebLogin) {
|
if (!input.secret && getRealDebridAccounts(this.settings).some((entry) => entry.id === status.accountId)) {
|
||||||
this.manager.applyDebridAccountStatuses([status]);
|
this.manager.applyDebridAccountStatuses([status]);
|
||||||
}
|
}
|
||||||
return status;
|
return status;
|
||||||
@@ -613,12 +629,13 @@ export class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async refreshRealDebridWebStatus(): Promise<void> {
|
private async refreshRealDebridWebStatus(): Promise<void> {
|
||||||
if (!this.settings.realDebridUseWebLogin) {
|
const account = getRealDebridAccounts(this.settings).find((entry) => entry.kind === "web");
|
||||||
|
if (!account) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const status = sanitizeDebridAccountStatus(
|
const status = sanitizeDebridAccountStatus(
|
||||||
await checkRealDebridAccount(
|
await checkRealDebridAccount(
|
||||||
this.settings,
|
account,
|
||||||
undefined,
|
undefined,
|
||||||
Date.now(),
|
Date.now(),
|
||||||
(signal) => this.realDebridWebFallback.probeLoginState(signal)
|
(signal) => this.realDebridWebFallback.probeLoginState(signal)
|
||||||
@@ -658,15 +675,16 @@ export class AppController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async checkDebridAccounts(scope: AccountCheckScope = "active"): Promise<DebridAccountStatus[]> {
|
public async checkDebridAccounts(scope: AccountCheckScope = "active"): Promise<DebridAccountStatus[]> {
|
||||||
const statuses = sanitizeDebridAccountStatuses(
|
const checkedStatuses = sanitizeDebridAccountStatuses(
|
||||||
await checkAllDebridAccounts(
|
await checkAllDebridAccounts(
|
||||||
this.settings,
|
this.settings,
|
||||||
undefined,
|
undefined,
|
||||||
(signal) => this.realDebridWebFallback.probeLoginState(signal),
|
(_accountId, signal) => this.realDebridWebFallback.probeLoginState(signal),
|
||||||
scope
|
scope
|
||||||
),
|
),
|
||||||
collectAccountStatusRedactionValues(this.settings)
|
collectAccountStatusRedactionValues(this.settings)
|
||||||
);
|
);
|
||||||
|
const statuses = retainConfiguredRealDebridStatuses(this.settings, checkedStatuses);
|
||||||
this.manager.applyDebridAccountStatuses(statuses);
|
this.manager.applyDebridAccountStatuses(statuses);
|
||||||
this.audit("INFO", "Debrid-Accounts geprueft", {
|
this.audit("INFO", "Debrid-Accounts geprueft", {
|
||||||
total: statuses.length,
|
total: statuses.length,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
||||||
import { getMegaDebridAccountsForMode, getMegaDebridDisabledAccountIdsForMode } from "../shared/mega-debrid-accounts";
|
import { getMegaDebridAccountsForMode, getMegaDebridDisabledAccountIdsForMode } from "../shared/mega-debrid-accounts";
|
||||||
|
import { getRealDebridAccounts } from "../shared/real-debrid-accounts";
|
||||||
import type { AppSettings, DebridAccountStatus, DebridProvider, RendererAccount, RendererAccountKind, RendererSettings } from "../shared/types";
|
import type { AppSettings, DebridAccountStatus, DebridProvider, RendererAccount, RendererAccountKind, RendererSettings } from "../shared/types";
|
||||||
import { collectAccountStatusRedactionValues, sanitizeDebridAccountStatus } from "./account-status-sanitizer";
|
import { collectAccountStatusRedactionValues, sanitizeDebridAccountStatus } from "./account-status-sanitizer";
|
||||||
|
|
||||||
@@ -51,7 +52,24 @@ function singleAccount(
|
|||||||
export function createRendererAccounts(settings: AppSettings): RendererAccount[] {
|
export function createRendererAccounts(settings: AppSettings): RendererAccount[] {
|
||||||
const accounts: RendererAccount[] = [];
|
const accounts: RendererAccount[] = [];
|
||||||
const redactions = collectAccountStatusRedactionValues(settings);
|
const redactions = collectAccountStatusRedactionValues(settings);
|
||||||
if (settings.realDebridUseWebLogin || settings.token.trim()) {
|
const realDebridAccounts = getRealDebridAccounts(settings);
|
||||||
|
for (const account of realDebridAccounts) {
|
||||||
|
const status = safeStatus(settings.debridAccountStatuses[account.id], redactions);
|
||||||
|
accounts.push({
|
||||||
|
accountId: account.id,
|
||||||
|
kind: account.kind === "web" ? "realdebrid-web" : "realdebrid-api",
|
||||||
|
provider: "realdebrid",
|
||||||
|
identity: status?.username || "",
|
||||||
|
maskedIdentity: account.maskedLogin,
|
||||||
|
hasSecret: true,
|
||||||
|
enabled: providerEnabled(settings, "realdebrid") && account.enabled,
|
||||||
|
dailyLimitBytes: settings.realDebridAccountDailyLimitBytes[account.id] || 0,
|
||||||
|
dailyUsageBytes: settings.realDebridAccountDailyUsageBytes[account.id] || 0,
|
||||||
|
totalUsageBytes: settings.realDebridAccountTotalUsageBytes[account.id] || 0,
|
||||||
|
status
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (realDebridAccounts.length === 0 && (settings.realDebridUseWebLogin || settings.token.trim())) {
|
||||||
accounts.push(singleAccount(
|
accounts.push(singleAccount(
|
||||||
settings,
|
settings,
|
||||||
settings.realDebridUseWebLogin ? "realdebrid-web" : "realdebrid-api",
|
settings.realDebridUseWebLogin ? "realdebrid-web" : "realdebrid-api",
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
import { getDebridLinkApiKeyIds } from "../shared/debrid-link-keys";
|
import { getDebridLinkApiKeyIds } from "../shared/debrid-link-keys";
|
||||||
import { getMegaDebridAccountIds, mergeMegaDebridCredentialPools } from "../shared/mega-debrid-accounts";
|
import { getMegaDebridAccountIds, mergeMegaDebridCredentialPools } from "../shared/mega-debrid-accounts";
|
||||||
|
import { getRealDebridAccountIds } from "../shared/real-debrid-accounts";
|
||||||
import type { AppSettings } from "../shared/types";
|
import type { AppSettings } from "../shared/types";
|
||||||
|
|
||||||
export function overlayLiveUsageCounters(target: AppSettings, liveSettings: AppSettings, liveTotalRuntimeMs: number): void {
|
export function overlayLiveUsageCounters(target: AppSettings, liveSettings: AppSettings, liveTotalRuntimeMs: number): void {
|
||||||
const debridLinkKeyIds = new Set(getDebridLinkApiKeyIds(target.debridLinkApiKeys));
|
const debridLinkKeyIds = new Set(getDebridLinkApiKeyIds(target.debridLinkApiKeys));
|
||||||
const megaAccountIds = new Set(getMegaDebridAccountIds(mergeMegaDebridCredentialPools(target.megaDebridApiCredentials || "", target.megaDebridWebCredentials || "") || target.megaCredentials || "", target.megaPassword || ""));
|
const megaAccountIds = new Set(getMegaDebridAccountIds(mergeMegaDebridCredentialPools(target.megaDebridApiCredentials || "", target.megaDebridWebCredentials || "") || target.megaCredentials || "", target.megaPassword || ""));
|
||||||
|
const realDebridAccountIds = new Set(getRealDebridAccountIds(target));
|
||||||
const validAccountIds = new Set([
|
const validAccountIds = new Set([
|
||||||
...debridLinkKeyIds,
|
...debridLinkKeyIds,
|
||||||
...megaAccountIds,
|
...megaAccountIds,
|
||||||
...(target.realDebridUseWebLogin || target.token.trim() ? ["svc-realdebrid"] : [])
|
...realDebridAccountIds,
|
||||||
|
...(realDebridAccountIds.size === 0 && (target.realDebridUseWebLogin || target.token.trim()) ? ["svc-realdebrid"] : [])
|
||||||
]);
|
]);
|
||||||
target.totalDownloadedAllTime = Math.max(target.totalDownloadedAllTime || 0, liveSettings.totalDownloadedAllTime || 0);
|
target.totalDownloadedAllTime = Math.max(target.totalDownloadedAllTime || 0, liveSettings.totalDownloadedAllTime || 0);
|
||||||
target.totalCompletedFilesAllTime = Math.max(target.totalCompletedFilesAllTime || 0, liveSettings.totalCompletedFilesAllTime || 0);
|
target.totalCompletedFilesAllTime = Math.max(target.totalCompletedFilesAllTime || 0, liveSettings.totalCompletedFilesAllTime || 0);
|
||||||
@@ -28,6 +31,12 @@ export function overlayLiveUsageCounters(target: AppSettings, liveSettings: AppS
|
|||||||
target.megaDebridAccountTotalUsageBytes = Object.fromEntries(
|
target.megaDebridAccountTotalUsageBytes = Object.fromEntries(
|
||||||
Object.entries(liveSettings.megaDebridAccountTotalUsageBytes || {}).filter(([accountId]) => megaAccountIds.has(accountId))
|
Object.entries(liveSettings.megaDebridAccountTotalUsageBytes || {}).filter(([accountId]) => megaAccountIds.has(accountId))
|
||||||
);
|
);
|
||||||
|
target.realDebridAccountDailyUsageBytes = Object.fromEntries(
|
||||||
|
Object.entries(liveSettings.realDebridAccountDailyUsageBytes || {}).filter(([accountId]) => realDebridAccountIds.has(accountId))
|
||||||
|
);
|
||||||
|
target.realDebridAccountTotalUsageBytes = Object.fromEntries(
|
||||||
|
Object.entries(liveSettings.realDebridAccountTotalUsageBytes || {}).filter(([accountId]) => realDebridAccountIds.has(accountId))
|
||||||
|
);
|
||||||
target.debridAccountStatuses = Object.fromEntries(
|
target.debridAccountStatuses = Object.fromEntries(
|
||||||
Object.entries(liveSettings.debridAccountStatuses || {}).filter(([accountId]) => validAccountIds.has(accountId))
|
Object.entries(liveSettings.debridAccountStatuses || {}).filter(([accountId]) => validAccountIds.has(accountId))
|
||||||
);
|
);
|
||||||
|
|||||||
+8
-6
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
|||||||
import fsp from "node:fs/promises";
|
import fsp from "node:fs/promises";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
import { getDebridLinkApiKeyIds } from "../shared/debrid-link-keys";
|
import { getDebridLinkApiKeyIds } from "../shared/debrid-link-keys";
|
||||||
import { getMegaDebridAccountIds, mergeMegaDebridCredentialPools, parseMegaDebridAccounts } from "../shared/mega-debrid-accounts";
|
import { getMegaDebridAccountIds, mergeMegaDebridCredentialPools, parseMegaDebridAccounts } from "../shared/mega-debrid-accounts";
|
||||||
import { AppSettings, AudioStripSummary, BandwidthScheduleEntry, DebridAccountStatus, DebridFallbackProvider, DebridProvider, DownloadItem, DownloadStatus, HistoryEntry, HistoryRetentionMode, LogStorageLocation, PackageEntry, PackagePriority, SessionState } from "../shared/types";
|
import { AppSettings, AudioStripSummary, BandwidthScheduleEntry, DebridAccountStatus, DebridFallbackProvider, DebridProvider, DownloadItem, DownloadStatus, HistoryEntry, HistoryRetentionMode, LogStorageLocation, PackageEntry, PackagePriority, SessionState } from "../shared/types";
|
||||||
@@ -447,12 +448,13 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
|||||||
const megaDebridDisabledAccountIds = [...new Set([...megaDebridApiDisabledAccountIds, ...megaDebridWebDisabledAccountIds])];
|
const megaDebridDisabledAccountIds = [...new Set([...megaDebridApiDisabledAccountIds, ...megaDebridWebDisabledAccountIds])];
|
||||||
const legacyRealDebridToken = asText(settings.token);
|
const legacyRealDebridToken = asText(settings.token);
|
||||||
const hasRealDebridApiPool = Object.prototype.hasOwnProperty.call(settings, "realDebridApiTokens");
|
const hasRealDebridApiPool = Object.prototype.hasOwnProperty.call(settings, "realDebridApiTokens");
|
||||||
const storedRealDebridApiTokens = serializeRealDebridApiAccounts(
|
const rawRealDebridApiAccounts = hasRealDebridApiPool
|
||||||
parseRealDebridApiAccounts(String(settings.realDebridApiTokens ?? "")).map((entry) => entry.token)
|
? parseRealDebridApiAccounts(String(settings.realDebridApiTokens ?? ""))
|
||||||
);
|
: legacyRealDebridToken ? parseRealDebridApiAccounts(legacyRealDebridToken) : [];
|
||||||
const realDebridApiTokens = hasRealDebridApiPool
|
const realDebridApiTokens = serializeRealDebridApiAccounts(rawRealDebridApiAccounts.map((account) => ({
|
||||||
? storedRealDebridApiTokens
|
id: account.id.startsWith("rda_legacy_") ? `rda_${randomUUID().replace(/-/g, "")}` : account.id,
|
||||||
: serializeRealDebridApiAccounts([legacyRealDebridToken]);
|
token: account.token
|
||||||
|
})));
|
||||||
const hasRealDebridWebPool = Object.prototype.hasOwnProperty.call(settings, "realDebridWebAccountIds");
|
const hasRealDebridWebPool = Object.prototype.hasOwnProperty.call(settings, "realDebridWebAccountIds");
|
||||||
let realDebridWebAccountIds = normalizeRealDebridWebAccountIds(settings.realDebridWebAccountIds);
|
let realDebridWebAccountIds = normalizeRealDebridWebAccountIds(settings.realDebridWebAccountIds);
|
||||||
if (!hasRealDebridWebPool && Boolean(settings.realDebridUseWebLogin)) {
|
if (!hasRealDebridWebPool && Boolean(settings.realDebridUseWebLogin)) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ export type AccountKind = RendererAccountKind;
|
|||||||
export type SingleAccountKind = Exclude<AccountKind, "megadebrid-api" | "megadebrid-web" | "debridlink-api">;
|
export type SingleAccountKind = Exclude<AccountKind, "megadebrid-api" | "megadebrid-web" | "debridlink-api">;
|
||||||
|
|
||||||
export type AccountEditTarget =
|
export type AccountEditTarget =
|
||||||
| { type: "single"; rowKey: string; kind: SingleAccountKind; service: AccountService; provider: DebridProvider }
|
| { type: "single"; rowKey: string; kind: SingleAccountKind; service: AccountService; provider: DebridProvider; accountId?: string }
|
||||||
| { type: "mega"; rowKey: string; kind: "megadebrid-api" | "megadebrid-web"; service: "megadebrid-api" | "megadebrid-web"; accountId: string }
|
| { type: "mega"; rowKey: string; kind: "megadebrid-api" | "megadebrid-web"; service: "megadebrid-api" | "megadebrid-web"; accountId: string }
|
||||||
| { type: "debridlink"; rowKey: string; kind: "debridlink-api"; service: "debridlink"; keyId: string };
|
| { type: "debridlink"; rowKey: string; kind: "debridlink-api"; service: "debridlink"; keyId: string };
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ function parseDailyLimit(value: string, originalBytes: number): number {
|
|||||||
function targetAccountId(target: AccountEditTarget): string {
|
function targetAccountId(target: AccountEditTarget): string {
|
||||||
if (target.type === "mega") return target.accountId;
|
if (target.type === "mega") return target.accountId;
|
||||||
if (target.type === "debridlink") return target.keyId;
|
if (target.type === "debridlink") return target.keyId;
|
||||||
return `svc-${target.provider}`;
|
return target.accountId || `svc-${target.provider}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createAccountEditState(target: AccountEditTarget, accounts: readonly RendererAccount[]): AccountEditState {
|
export function createAccountEditState(target: AccountEditTarget, accounts: readonly RendererAccount[]): AccountEditState {
|
||||||
|
|||||||
@@ -18,113 +18,79 @@ export interface RealDebridWebAccountEntry extends RealDebridAccountBase {
|
|||||||
|
|
||||||
export type RealDebridAccountEntry = RealDebridApiAccountEntry | RealDebridWebAccountEntry;
|
export type RealDebridAccountEntry = RealDebridApiAccountEntry | RealDebridWebAccountEntry;
|
||||||
|
|
||||||
|
export type RealDebridApiCredential = {
|
||||||
|
id: string;
|
||||||
|
token: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type RealDebridAccountSettings = {
|
export type RealDebridAccountSettings = {
|
||||||
realDebridApiTokens?: string;
|
realDebridApiTokens?: string;
|
||||||
realDebridWebAccountIds?: string[];
|
realDebridWebAccountIds?: string[];
|
||||||
realDebridDisabledAccountIds?: string[];
|
realDebridDisabledAccountIds?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const API_ACCOUNT_ID_RE = /^rda_[A-Za-z0-9_-]{1,96}$/;
|
||||||
const WEB_ACCOUNT_ID_RE = /^rdw_[A-Za-z0-9_-]{1,96}$/;
|
const WEB_ACCOUNT_ID_RE = /^rdw_[A-Za-z0-9_-]{1,96}$/;
|
||||||
const SHA256_INITIAL = [
|
|
||||||
0x6a09e667, 0xbb67ae85, 0x3c6ef372, 0xa54ff53a,
|
|
||||||
0x510e527f, 0x9b05688c, 0x1f83d9ab, 0x5be0cd19
|
|
||||||
] as const;
|
|
||||||
const SHA256_CONSTANTS = [
|
|
||||||
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
|
||||||
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
|
||||||
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
|
||||||
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
|
||||||
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
|
||||||
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
|
||||||
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
|
||||||
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2
|
|
||||||
] as const;
|
|
||||||
|
|
||||||
function rotateRight(value: number, count: number): number {
|
export function isRealDebridApiAccountId(value: string): boolean {
|
||||||
return (value >>> count) | (value << (32 - count));
|
return API_ACCOUNT_ID_RE.test(String(value || "").trim());
|
||||||
}
|
|
||||||
|
|
||||||
function sha256(text: string): string {
|
|
||||||
const bytes = new TextEncoder().encode(text);
|
|
||||||
const paddingLength = (64 - ((bytes.length + 1 + 8) % 64)) % 64;
|
|
||||||
const data = new Uint8Array(bytes.length + 1 + paddingLength + 8);
|
|
||||||
data.set(bytes);
|
|
||||||
data[bytes.length] = 0x80;
|
|
||||||
let bitLength = BigInt(bytes.length) * 8n;
|
|
||||||
for (let index = 0; index < 8; index += 1) {
|
|
||||||
data[data.length - 1 - index] = Number(bitLength & 0xffn);
|
|
||||||
bitLength >>= 8n;
|
|
||||||
}
|
|
||||||
const hash: number[] = [...SHA256_INITIAL];
|
|
||||||
const words = new Uint32Array(64);
|
|
||||||
for (let offset = 0; offset < data.length; offset += 64) {
|
|
||||||
const view = new DataView(data.buffer, data.byteOffset + offset, 64);
|
|
||||||
for (let index = 0; index < 16; index += 1) {
|
|
||||||
words[index] = view.getUint32(index * 4, false);
|
|
||||||
}
|
|
||||||
for (let index = 16; index < 64; index += 1) {
|
|
||||||
const previous15 = words[index - 15];
|
|
||||||
const previous2 = words[index - 2];
|
|
||||||
const sigma0 = rotateRight(previous15, 7) ^ rotateRight(previous15, 18) ^ (previous15 >>> 3);
|
|
||||||
const sigma1 = rotateRight(previous2, 17) ^ rotateRight(previous2, 19) ^ (previous2 >>> 10);
|
|
||||||
words[index] = (words[index - 16] + sigma0 + words[index - 7] + sigma1) >>> 0;
|
|
||||||
}
|
|
||||||
let [a, b, c, d, e, f, g, h] = hash;
|
|
||||||
for (let index = 0; index < 64; index += 1) {
|
|
||||||
const upperSigma1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
|
|
||||||
const choice = (e & f) ^ (~e & g);
|
|
||||||
const temporary1 = (h + upperSigma1 + choice + SHA256_CONSTANTS[index] + words[index]) >>> 0;
|
|
||||||
const upperSigma0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
|
|
||||||
const majority = (a & b) ^ (a & c) ^ (b & c);
|
|
||||||
const temporary2 = (upperSigma0 + majority) >>> 0;
|
|
||||||
h = g;
|
|
||||||
g = f;
|
|
||||||
f = e;
|
|
||||||
e = (d + temporary1) >>> 0;
|
|
||||||
d = c;
|
|
||||||
c = b;
|
|
||||||
b = a;
|
|
||||||
a = (temporary1 + temporary2) >>> 0;
|
|
||||||
}
|
|
||||||
hash[0] = (hash[0] + a) >>> 0;
|
|
||||||
hash[1] = (hash[1] + b) >>> 0;
|
|
||||||
hash[2] = (hash[2] + c) >>> 0;
|
|
||||||
hash[3] = (hash[3] + d) >>> 0;
|
|
||||||
hash[4] = (hash[4] + e) >>> 0;
|
|
||||||
hash[5] = (hash[5] + f) >>> 0;
|
|
||||||
hash[6] = (hash[6] + g) >>> 0;
|
|
||||||
hash[7] = (hash[7] + h) >>> 0;
|
|
||||||
}
|
|
||||||
return hash.map((value) => value.toString(16).padStart(8, "0")).join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeTokens(values: readonly string[]): string[] {
|
|
||||||
const seen = new Set<string>();
|
|
||||||
const result: string[] = [];
|
|
||||||
for (const value of values) {
|
|
||||||
const token = String(value || "").trim();
|
|
||||||
if (!token || seen.has(token)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
seen.add(token);
|
|
||||||
result.push(token);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getRealDebridApiAccountId(token: string): string {
|
|
||||||
return `rda_${sha256(String(token || "").trim()).slice(0, 32)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isRealDebridWebAccountId(value: string): boolean {
|
export function isRealDebridWebAccountId(value: string): boolean {
|
||||||
return WEB_ACCOUNT_ID_RE.test(String(value || "").trim());
|
return WEB_ACCOUNT_ID_RE.test(String(value || "").trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeCredentials(values: readonly RealDebridApiCredential[]): RealDebridApiCredential[] {
|
||||||
|
const ids = new Set<string>();
|
||||||
|
const tokens = new Set<string>();
|
||||||
|
const result: RealDebridApiCredential[] = [];
|
||||||
|
for (const value of values) {
|
||||||
|
const id = String(value?.id || "").trim();
|
||||||
|
const token = String(value?.token || "").trim();
|
||||||
|
if (!isRealDebridApiAccountId(id) || !token || /[\r\n]/.test(token) || ids.has(id) || tokens.has(token)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ids.add(id);
|
||||||
|
tokens.add(token);
|
||||||
|
result.push({ id, token });
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePersistedCredentials(raw: string): RealDebridApiCredential[] | null {
|
||||||
|
try {
|
||||||
|
const parsed = JSON.parse(raw) as { version?: unknown; accounts?: unknown };
|
||||||
|
if (parsed?.version !== 1 || !Array.isArray(parsed.accounts)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return normalizeCredentials(parsed.accounts as RealDebridApiCredential[]);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLegacyCredentials(raw: string): RealDebridApiCredential[] {
|
||||||
|
const tokens = new Set<string>();
|
||||||
|
const result: RealDebridApiCredential[] = [];
|
||||||
|
for (const value of raw.split(/[\n,]+/)) {
|
||||||
|
const token = value.trim();
|
||||||
|
if (!token || /\r/.test(token) || tokens.has(token)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
tokens.add(token);
|
||||||
|
result.push({ id: `rda_legacy_${result.length + 1}`, token });
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
export function parseRealDebridApiAccounts(raw: string): RealDebridApiAccountEntry[] {
|
export function parseRealDebridApiAccounts(raw: string): RealDebridApiAccountEntry[] {
|
||||||
return normalizeTokens(String(raw || "").split(/[\n,]+/)).map((token, index) => ({
|
const normalizedRaw = String(raw || "").trim();
|
||||||
id: getRealDebridApiAccountId(token),
|
const credentials = normalizedRaw
|
||||||
|
? parsePersistedCredentials(normalizedRaw) ?? parseLegacyCredentials(normalizedRaw)
|
||||||
|
: [];
|
||||||
|
return credentials.map((entry, index) => ({
|
||||||
|
...entry,
|
||||||
kind: "api",
|
kind: "api",
|
||||||
token,
|
|
||||||
index,
|
index,
|
||||||
label: `API-Token ${index + 1}`,
|
label: `API-Token ${index + 1}`,
|
||||||
maskedLogin: "Geschützter API-Token",
|
maskedLogin: "Geschützter API-Token",
|
||||||
@@ -132,8 +98,9 @@ export function parseRealDebridApiAccounts(raw: string): RealDebridApiAccountEnt
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serializeRealDebridApiAccounts(tokens: readonly string[]): string {
|
export function serializeRealDebridApiAccounts(accounts: readonly RealDebridApiCredential[]): string {
|
||||||
return normalizeTokens(tokens).join("\n");
|
const normalized = normalizeCredentials(accounts);
|
||||||
|
return normalized.length > 0 ? JSON.stringify({ version: 1, accounts: normalized }) : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeRealDebridWebAccountIds(raw: unknown): string[] {
|
export function normalizeRealDebridWebAccountIds(raw: unknown): string[] {
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
import { describe, it, expect, vi, afterEach } from "vitest";
|
||||||
import { checkMegaDebridAccount, checkDebridLinkKey, checkAllDebridAccounts, checkRealDebridAccount, REAL_DEBRID_STATUS_ID } from "../src/main/account-check";
|
import { checkMegaDebridAccount, checkDebridLinkKey, checkAllDebridAccounts, checkRealDebridAccount, REAL_DEBRID_STATUS_ID, retainConfiguredRealDebridStatuses } from "../src/main/account-check";
|
||||||
import type { MegaDebridAccountEntry } from "../src/shared/mega-debrid-accounts";
|
import type { MegaDebridAccountEntry } from "../src/shared/mega-debrid-accounts";
|
||||||
import { getDebridLinkApiKeyId, type DebridLinkApiKeyEntry } from "../src/shared/debrid-link-keys";
|
import { getDebridLinkApiKeyId, type DebridLinkApiKeyEntry } from "../src/shared/debrid-link-keys";
|
||||||
import type { AppSettings } from "../src/shared/types";
|
import type { AppSettings } from "../src/shared/types";
|
||||||
import { defaultSettings } from "../src/main/constants";
|
import { defaultSettings } from "../src/main/constants";
|
||||||
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||||
|
import { getRealDebridAccounts, serializeRealDebridApiAccounts } from "../src/shared/real-debrid-accounts";
|
||||||
|
|
||||||
function megaAccount(login = "user@example.com"): MegaDebridAccountEntry {
|
function megaAccount(login = "user@example.com"): MegaDebridAccountEntry {
|
||||||
return { id: "mda_test", login, password: "pw", index: 0, label: "Account 1", maskedLogin: "us**le" };
|
return { id: "mda_test", login, password: "pw", index: 0, label: "Account 1", maskedLogin: "us**le" };
|
||||||
@@ -122,6 +123,13 @@ describe("checkDebridLinkKey", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("checkRealDebridAccount", () => {
|
describe("checkRealDebridAccount", () => {
|
||||||
|
it("keys every API account status by its concrete pool identity", async () => {
|
||||||
|
const token = "rd-api-pool-token";
|
||||||
|
mockFetchOnce(200, { username: "api-user", type: "premium", expiration: new Date(NOW + 100_000).toISOString() });
|
||||||
|
const account = getRealDebridAccounts({ realDebridApiTokens: serializeRealDebridApiAccounts([{ id: "rda_checkOpaque", token }]) })[0];
|
||||||
|
const status = await checkRealDebridAccount(account, undefined, NOW);
|
||||||
|
expect(status.accountId).toBe("rda_checkOpaque");
|
||||||
|
});
|
||||||
it("keeps browser-session username and email in separate status fields", async () => {
|
it("keeps browser-session username and email in separate status fields", async () => {
|
||||||
const premiumUntilMs = NOW + 30 * 24 * 60 * 60 * 1000;
|
const premiumUntilMs = NOW + 30 * 24 * 60 * 60 * 1000;
|
||||||
const probe = vi.fn(async () => ({
|
const probe = vi.fn(async () => ({
|
||||||
@@ -173,6 +181,36 @@ describe("checkRealDebridAccount", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("checkAllDebridAccounts", () => {
|
describe("checkAllDebridAccounts", () => {
|
||||||
|
it("discards a late Real-Debrid result after its account was removed", () => {
|
||||||
|
const removedId = "rda_removedAfterCheck";
|
||||||
|
const lateStatus = { accountId: removedId, provider: "realdebrid" as const, label: "API-Token 1", maskedLogin: "Geschützt", valid: true, isPremium: true, premiumUntilMs: null, message: "Premium aktiv", checkedAt: NOW };
|
||||||
|
expect(retainConfiguredRealDebridStatuses(defaultSettings(), [lateStatus])).toEqual([]);
|
||||||
|
});
|
||||||
|
it("checks only enabled Real-Debrid pool entries in active scope and all entries in all scope", async () => {
|
||||||
|
const firstToken = "rd-pool-active";
|
||||||
|
const secondToken = "rd-pool-disabled";
|
||||||
|
const activeId = "rda_poolActive";
|
||||||
|
const disabledId = "rda_poolDisabled";
|
||||||
|
const settings = {
|
||||||
|
...defaultSettings(),
|
||||||
|
realDebridApiTokens: serializeRealDebridApiAccounts([{ id: activeId, token: firstToken }, { id: disabledId, token: secondToken }]),
|
||||||
|
realDebridWebAccountIds: ["rdw_first", "rdw_second"],
|
||||||
|
realDebridDisabledAccountIds: [disabledId, "rdw_second"]
|
||||||
|
};
|
||||||
|
vi.stubGlobal("fetch", vi.fn(async () => ({ ok: true, status: 200, text: async () => JSON.stringify({ username: "api", type: "premium" }) })) as unknown as typeof fetch);
|
||||||
|
const probedAccountIds: string[] = [];
|
||||||
|
const probe = vi.fn(async (accountId: string) => {
|
||||||
|
probedAccountIds.push(accountId);
|
||||||
|
return { valid: true, isPremium: true, username: "web" };
|
||||||
|
});
|
||||||
|
|
||||||
|
const active = await checkAllDebridAccounts(settings, undefined, probe, "active");
|
||||||
|
const all = await checkAllDebridAccounts(settings, undefined, probe, "all");
|
||||||
|
|
||||||
|
expect(active.map((status) => status.accountId)).toEqual([activeId, "rdw_first"]);
|
||||||
|
expect(all.map((status) => status.accountId)).toEqual([activeId, disabledId, "rdw_first", "rdw_second"]);
|
||||||
|
expect(probedAccountIds).toEqual(["rdw_first", "rdw_first", "rdw_second"]);
|
||||||
|
});
|
||||||
it("returns empty array when nothing configured", async () => {
|
it("returns empty array when nothing configured", async () => {
|
||||||
const settings = { megaCredentials: "", megaPassword: "", debridLinkApiKeys: "" } as unknown as AppSettings;
|
const settings = { megaCredentials: "", megaPassword: "", debridLinkApiKeys: "" } as unknown as AppSettings;
|
||||||
const result = await checkAllDebridAccounts(settings);
|
const result = await checkAllDebridAccounts(settings);
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { applyAccountCommand, validateAccountCommand, validateAccountCredentialCheckInput } from "../src/main/account-commands";
|
import { applyAccountCommand, resolveStoredAccountSecret, setRealDebridAccountEnabled, validateAccountCommand, validateAccountCredentialCheckInput } from "../src/main/account-commands";
|
||||||
import * as accountCommands from "../src/main/account-commands";
|
import * as accountCommands from "../src/main/account-commands";
|
||||||
import { defaultSettings } from "../src/main/constants";
|
import { defaultSettings } from "../src/main/constants";
|
||||||
import { getDebridLinkApiKeyId } from "../src/shared/debrid-link-keys";
|
import { getDebridLinkApiKeyId } from "../src/shared/debrid-link-keys";
|
||||||
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||||
|
import { parseRealDebridApiAccounts, serializeRealDebridApiAccounts } from "../src/shared/real-debrid-accounts";
|
||||||
import type { AppSettings, RendererAccountKind } from "../src/shared/types";
|
import type { AppSettings, RendererAccountKind } from "../src/shared/types";
|
||||||
|
|
||||||
const ORIGINAL_SECRET = "fixture-original-secret-4qV8";
|
const ORIGINAL_SECRET = "fixture-original-secret-4qV8";
|
||||||
@@ -43,6 +44,63 @@ const ACCOUNT_KINDS: RendererAccountKind[] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
describe("write-only account commands", () => {
|
describe("write-only account commands", () => {
|
||||||
|
it("manages multiple Real-Debrid API accounts without touching siblings", () => {
|
||||||
|
const firstToken = "fixture-rd-pool-first-1aB2";
|
||||||
|
const secondToken = "fixture-rd-pool-second-3cD4";
|
||||||
|
const replacementToken = "fixture-rd-pool-replacement-5eF6";
|
||||||
|
const first = applyAccountCommand(defaultSettings(), validateAccountCommand({ action: "create", kind: "realdebrid-api", secret: firstToken, dailyLimitBytes: 2 * GIB }));
|
||||||
|
const second = applyAccountCommand(first.settings, validateAccountCommand({ action: "create", kind: "realdebrid-api", secret: secondToken, dailyLimitBytes: 3 * GIB }));
|
||||||
|
|
||||||
|
expect(parseRealDebridApiAccounts(second.settings.realDebridApiTokens).map((entry) => entry.token)).toEqual([firstToken, secondToken]);
|
||||||
|
expect(second.response.accountId).toMatch(/^rda_[A-Za-z0-9_-]+$/);
|
||||||
|
expect(() => applyAccountCommand(second.settings, validateAccountCommand({ action: "create", kind: "realdebrid-api", secret: secondToken }))).toThrow(/ungültig/i);
|
||||||
|
expect(resolveStoredAccountSecret(second.settings, { kind: "realdebrid-api", accountId: first.response.accountId! })).toBe(firstToken);
|
||||||
|
expect(resolveStoredAccountSecret(second.settings, { kind: "realdebrid-api", accountId: second.response.accountId! })).toBe(secondToken);
|
||||||
|
|
||||||
|
const replaced = applyAccountCommand(second.settings, validateAccountCommand({
|
||||||
|
action: "replace",
|
||||||
|
kind: "realdebrid-api",
|
||||||
|
accountId: second.response.accountId,
|
||||||
|
secret: replacementToken,
|
||||||
|
dailyLimitBytes: 4 * GIB
|
||||||
|
}));
|
||||||
|
const replacementId = second.response.accountId!;
|
||||||
|
expect(parseRealDebridApiAccounts(replaced.settings.realDebridApiTokens).map((entry) => entry.token)).toEqual([firstToken, replacementToken]);
|
||||||
|
expect(replaced.settings.realDebridAccountDailyLimitBytes).toEqual({ [first.response.accountId!]: 2 * GIB, [replacementId]: 4 * GIB });
|
||||||
|
|
||||||
|
const deleted = applyAccountCommand(replaced.settings, validateAccountCommand({ action: "delete", kind: "realdebrid-api", accountId: first.response.accountId }));
|
||||||
|
expect(parseRealDebridApiAccounts(deleted.settings.realDebridApiTokens).map((entry) => entry.token)).toEqual([replacementToken]);
|
||||||
|
expect(deleted.response.accountId).toBe(replacementId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("creates, updates and deletes one Real-Debrid Web account by opaque ID", () => {
|
||||||
|
const first = applyAccountCommand(defaultSettings(), validateAccountCommand({ action: "create", kind: "realdebrid-web", identity: "rdw_first", dailyLimitBytes: GIB }));
|
||||||
|
const second = applyAccountCommand(first.settings, validateAccountCommand({ action: "create", kind: "realdebrid-web", identity: "rdw_second", dailyLimitBytes: 2 * GIB }));
|
||||||
|
expect(second.settings.realDebridWebAccountIds).toEqual(["rdw_first", "rdw_second"]);
|
||||||
|
expect(() => applyAccountCommand(second.settings, validateAccountCommand({ action: "create", kind: "realdebrid-web", identity: "rdw_second" }))).toThrow(/ungültig/i);
|
||||||
|
|
||||||
|
const replaced = applyAccountCommand(second.settings, validateAccountCommand({ action: "replace", kind: "realdebrid-web", accountId: "rdw_second", dailyLimitBytes: 3 * GIB }));
|
||||||
|
expect(replaced.settings.realDebridAccountDailyLimitBytes).toEqual({ rdw_first: GIB, rdw_second: 3 * GIB });
|
||||||
|
|
||||||
|
const deleted = applyAccountCommand(replaced.settings, validateAccountCommand({ action: "delete", kind: "realdebrid-web", accountId: "rdw_first" }));
|
||||||
|
expect(deleted.settings.realDebridWebAccountIds).toEqual(["rdw_second"]);
|
||||||
|
expect(deleted.settings.realDebridAccountDailyLimitBytes).toEqual({ rdw_second: 3 * GIB });
|
||||||
|
});
|
||||||
|
|
||||||
|
it("toggles only the selected Real-Debrid account ID", () => {
|
||||||
|
const firstToken = "fixture-rd-toggle-first";
|
||||||
|
const secondToken = "fixture-rd-toggle-second";
|
||||||
|
const firstId = "rda_toggleFirst";
|
||||||
|
const secondId = "rda_toggleSecond";
|
||||||
|
const settings = { ...defaultSettings(), realDebridApiTokens: serializeRealDebridApiAccounts([{ id: firstId, token: firstToken }, { id: secondId, token: secondToken }]) };
|
||||||
|
|
||||||
|
const disabled = setRealDebridAccountEnabled(settings, secondId, false);
|
||||||
|
expect(disabled.realDebridDisabledAccountIds).toEqual([secondId]);
|
||||||
|
expect(parseRealDebridApiAccounts(disabled.realDebridApiTokens).find((account) => account.id === firstId)?.enabled).toBe(true);
|
||||||
|
|
||||||
|
const enabled = setRealDebridAccountEnabled(disabled, secondId, true);
|
||||||
|
expect(enabled.realDebridDisabledAccountIds).toEqual([]);
|
||||||
|
});
|
||||||
it("reveals only the exact explicitly requested stored account secret", () => {
|
it("reveals only the exact explicitly requested stored account secret", () => {
|
||||||
const api = accountCommands as typeof accountCommands & {
|
const api = accountCommands as typeof accountCommands & {
|
||||||
resolveStoredAccountSecret?: (settings: AppSettings, request: { kind: RendererAccountKind; accountId: string }) => string;
|
resolveStoredAccountSecret?: (settings: AppSettings, request: { kind: RendererAccountKind; accountId: string }) => string;
|
||||||
@@ -67,6 +125,11 @@ describe("write-only account commands", () => {
|
|||||||
expect(api.resolveStoredAccountSecret).toBeTypeOf("function");
|
expect(api.resolveStoredAccountSecret).toBeTypeOf("function");
|
||||||
expect(api.validateAccountSecretRequest).toBeTypeOf("function");
|
expect(api.validateAccountSecretRequest).toBeTypeOf("function");
|
||||||
expect(api.resolveStoredAccountSecret?.(settings, { kind: "realdebrid-api", accountId: "svc-realdebrid" })).toBe("fixture-reveal-rd-1aB2");
|
expect(api.resolveStoredAccountSecret?.(settings, { kind: "realdebrid-api", accountId: "svc-realdebrid" })).toBe("fixture-reveal-rd-1aB2");
|
||||||
|
const pooled = {
|
||||||
|
...settings,
|
||||||
|
realDebridApiTokens: serializeRealDebridApiAccounts([{ id: "rda_revealOpaque", token: "fixture-reveal-rd-1aB2" }])
|
||||||
|
};
|
||||||
|
expect(() => api.resolveStoredAccountSecret?.(pooled, { kind: "realdebrid-api", accountId: "svc-realdebrid" })).toThrow(/nicht gefunden/i);
|
||||||
expect(api.resolveStoredAccountSecret?.(settings, { kind: "megadebrid-api", accountId: megaId })).toBe("fixture-reveal-mega-3cD4");
|
expect(api.resolveStoredAccountSecret?.(settings, { kind: "megadebrid-api", accountId: megaId })).toBe("fixture-reveal-mega-3cD4");
|
||||||
expect(api.resolveStoredAccountSecret?.(settings, { kind: "debridlink-api", accountId: getDebridLinkApiKeyId("fixture-reveal-dl-5eF6") })).toBe("fixture-reveal-dl-5eF6");
|
expect(api.resolveStoredAccountSecret?.(settings, { kind: "debridlink-api", accountId: getDebridLinkApiKeyId("fixture-reveal-dl-5eF6") })).toBe("fixture-reveal-dl-5eF6");
|
||||||
expect(api.resolveStoredAccountSecret?.(settings, { kind: "bestdebrid-api", accountId: "svc-bestdebrid" })).toBe("fixture-reveal-best-7gH8");
|
expect(api.resolveStoredAccountSecret?.(settings, { kind: "bestdebrid-api", accountId: "svc-bestdebrid" })).toBe("fixture-reveal-best-7gH8");
|
||||||
|
|||||||
@@ -1,67 +1,57 @@
|
|||||||
import crypto from "node:crypto";
|
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import {
|
import {
|
||||||
getRealDebridAccountIds,
|
getRealDebridAccountIds,
|
||||||
getRealDebridAccounts,
|
getRealDebridAccounts,
|
||||||
getRealDebridApiAccountId,
|
|
||||||
parseRealDebridApiAccounts,
|
parseRealDebridApiAccounts,
|
||||||
serializeRealDebridApiAccounts
|
serializeRealDebridApiAccounts
|
||||||
} from "../src/shared/real-debrid-accounts";
|
} from "../src/shared/real-debrid-accounts";
|
||||||
|
|
||||||
describe("Real-Debrid account pool", () => {
|
describe("Real-Debrid account pool", () => {
|
||||||
it("parses distinct API tokens and removes exact duplicates", () => {
|
it("reads persisted opaque API identities and removes duplicate tokens", () => {
|
||||||
const accounts = parseRealDebridApiAccounts("first-secret-token\r\nsecond-secret-token\nfirst-secret-token");
|
const raw = serializeRealDebridApiAccounts([
|
||||||
|
{ id: "rda_firstOpaque", token: "first-secret-token" },
|
||||||
|
{ id: "rda_secondOpaque", token: "second-secret-token" },
|
||||||
|
{ id: "rda_duplicateOpaque", token: "first-secret-token" }
|
||||||
|
]);
|
||||||
|
const accounts = parseRealDebridApiAccounts(raw);
|
||||||
|
|
||||||
expect(accounts).toHaveLength(2);
|
expect(accounts).toHaveLength(2);
|
||||||
|
expect(accounts.map((entry) => entry.id)).toEqual(["rda_firstOpaque", "rda_secondOpaque"]);
|
||||||
expect(accounts.map((entry) => entry.token)).toEqual(["first-secret-token", "second-secret-token"]);
|
expect(accounts.map((entry) => entry.token)).toEqual(["first-secret-token", "second-secret-token"]);
|
||||||
expect(accounts.map((entry) => entry.label)).toEqual(["API-Token 1", "API-Token 2"]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("creates stable opaque IDs without secret material", () => {
|
it("keeps opaque IDs independent from token material", () => {
|
||||||
const token = "private-real-debrid-token";
|
const token = "private-real-debrid-token";
|
||||||
const first = getRealDebridApiAccountId(token);
|
const raw = serializeRealDebridApiAccounts([{ id: "rda_opaqueAccount42", token }]);
|
||||||
const second = getRealDebridApiAccountId(` ${token} `);
|
const account = parseRealDebridApiAccounts(raw)[0];
|
||||||
|
|
||||||
expect(first).toBe(second);
|
expect(account.id).toBe("rda_opaqueAccount42");
|
||||||
expect(first).toMatch(/^rda_[a-z0-9]+$/);
|
expect(account.id).not.toContain(token);
|
||||||
expect(first).not.toContain(token);
|
expect(account.label).not.toContain(token);
|
||||||
expect(parseRealDebridApiAccounts(token)[0]).toMatchObject({ id: first, kind: "api" });
|
expect(account.maskedLogin).not.toContain(token);
|
||||||
expect(parseRealDebridApiAccounts(token)[0].label).not.toContain(token);
|
|
||||||
expect(parseRealDebridApiAccounts(token)[0].maskedLogin).not.toContain(token);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it.each([
|
it("serializes the versioned pool idempotently", () => {
|
||||||
"cryptographic-real-debrid-token",
|
const serialized = serializeRealDebridApiAccounts([
|
||||||
"üñîçødé-real-debrid-token",
|
{ id: "rda_firstOpaque", token: " first-secret-token " },
|
||||||
"x".repeat(160)
|
{ id: "rda_secondOpaque", token: "second-secret-token" }
|
||||||
])("derives API account IDs from a cryptographic SHA-256 fingerprint", (token) => {
|
]);
|
||||||
const digest = crypto.createHash("sha256").update(token, "utf8").digest("hex").slice(0, 32);
|
|
||||||
|
|
||||||
expect(getRealDebridApiAccountId(token)).toBe(`rda_${digest}`);
|
expect(serializeRealDebridApiAccounts(parseRealDebridApiAccounts(serialized))).toBe(serialized);
|
||||||
});
|
|
||||||
|
|
||||||
it("serializes normalized unique API tokens one per line", () => {
|
|
||||||
expect(serializeRealDebridApiAccounts([
|
|
||||||
" first-secret-token ",
|
|
||||||
"",
|
|
||||||
"second-secret-token",
|
|
||||||
"first-secret-token"
|
|
||||||
])).toBe("first-secret-token\nsecond-secret-token");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("combines API and opaque Web accounts and marks disabled entries", () => {
|
it("combines API and opaque Web accounts and marks disabled entries", () => {
|
||||||
const apiId = getRealDebridApiAccountId("api-secret");
|
|
||||||
const settings = {
|
const settings = {
|
||||||
realDebridApiTokens: "api-secret",
|
realDebridApiTokens: serializeRealDebridApiAccounts([{ id: "rda_apiOpaque", token: "api-secret" }]),
|
||||||
realDebridWebAccountIds: ["rdw_legacy", "rdw_second"],
|
realDebridWebAccountIds: ["rdw_legacy", "rdw_second"],
|
||||||
realDebridDisabledAccountIds: [apiId, "rdw_second"]
|
realDebridDisabledAccountIds: ["rda_apiOpaque", "rdw_second"]
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(getRealDebridAccounts(settings)).toEqual([
|
expect(getRealDebridAccounts(settings)).toEqual([
|
||||||
expect.objectContaining({ id: apiId, kind: "api", enabled: false }),
|
expect.objectContaining({ id: "rda_apiOpaque", kind: "api", enabled: false }),
|
||||||
expect.objectContaining({ id: "rdw_legacy", kind: "web", enabled: true }),
|
expect.objectContaining({ id: "rdw_legacy", kind: "web", enabled: true }),
|
||||||
expect.objectContaining({ id: "rdw_second", kind: "web", enabled: false })
|
expect.objectContaining({ id: "rdw_second", kind: "web", enabled: false })
|
||||||
]);
|
]);
|
||||||
expect(getRealDebridAccountIds(settings)).toEqual([apiId, "rdw_legacy", "rdw_second"]);
|
expect(getRealDebridAccountIds(settings)).toEqual(["rda_apiOpaque", "rdw_legacy", "rdw_second"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import crypto from "node:crypto";
|
||||||
import { defaultSettings } from "../src/main/constants";
|
import { defaultSettings } from "../src/main/constants";
|
||||||
import { createRendererState } from "../src/main/renderer-state";
|
import { createRendererState } from "../src/main/renderer-state";
|
||||||
import { getDebridLinkApiKeyId } from "../src/shared/debrid-link-keys";
|
import { getDebridLinkApiKeyId } from "../src/shared/debrid-link-keys";
|
||||||
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||||
|
import { serializeRealDebridApiAccounts } from "../src/shared/real-debrid-accounts";
|
||||||
import type { AppSettings, RendererAccountKind } from "../src/shared/types";
|
import type { AppSettings, RendererAccountKind } from "../src/shared/types";
|
||||||
|
|
||||||
const SECRETS = {
|
const SECRETS = {
|
||||||
@@ -38,6 +40,35 @@ const ACCOUNT_FIXTURES: Array<{
|
|||||||
];
|
];
|
||||||
|
|
||||||
describe("renderer state serialization", () => {
|
describe("renderer state serialization", () => {
|
||||||
|
it("projects distinct Real-Debrid API and Web rows with per-account state", () => {
|
||||||
|
const firstToken = "fixture-renderer-rd-first-1aB2";
|
||||||
|
const secondToken = "fixture-renderer-rd-second-3cD4";
|
||||||
|
const firstId = "rda_rendererFirst";
|
||||||
|
const secondId = "rda_rendererSecond";
|
||||||
|
const settings = {
|
||||||
|
...defaultSettings(),
|
||||||
|
realDebridApiTokens: serializeRealDebridApiAccounts([{ id: firstId, token: firstToken }, { id: secondId, token: secondToken }]),
|
||||||
|
realDebridWebAccountIds: ["rdw_first"],
|
||||||
|
realDebridDisabledAccountIds: [secondId],
|
||||||
|
realDebridAccountDailyLimitBytes: { [firstId]: 100 },
|
||||||
|
realDebridAccountDailyUsageBytes: { [firstId]: 25 },
|
||||||
|
realDebridAccountTotalUsageBytes: { [firstId]: 500 },
|
||||||
|
debridAccountStatuses: {
|
||||||
|
[firstId]: { accountId: firstId, provider: "realdebrid" as const, label: "API-Token 1", maskedLogin: "Geschützter API-Token", valid: true, isPremium: true, premiumUntilMs: null, message: "Premium aktiv", checkedAt: 1 }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const state = createRendererState(settings);
|
||||||
|
const realDebridRows = state.accounts.filter((account) => account.provider === "realdebrid");
|
||||||
|
|
||||||
|
expect(realDebridRows).toEqual([
|
||||||
|
expect.objectContaining({ accountId: firstId, kind: "realdebrid-api", enabled: true, dailyLimitBytes: 100, dailyUsageBytes: 25, totalUsageBytes: 500, status: expect.objectContaining({ accountId: firstId }) }),
|
||||||
|
expect.objectContaining({ accountId: secondId, kind: "realdebrid-api", enabled: false }),
|
||||||
|
expect.objectContaining({ accountId: "rdw_first", kind: "realdebrid-web", enabled: true })
|
||||||
|
]);
|
||||||
|
expect(JSON.stringify(state)).not.toContain(firstToken);
|
||||||
|
expect(JSON.stringify(state)).not.toContain(secondToken);
|
||||||
|
expect(realDebridRows[0].accountId).not.toBe(`rda_${crypto.createHash("sha256").update(firstToken).digest("hex").slice(0, 32)}`);
|
||||||
|
});
|
||||||
it.each(ACCOUNT_FIXTURES)("serializes $kind without its representative secret", ({ kind, secret, settings }) => {
|
it.each(ACCOUNT_FIXTURES)("serializes $kind without its representative secret", ({ kind, secret, settings }) => {
|
||||||
const state = createRendererState({ ...defaultSettings(), ...settings });
|
const state = createRendererState({ ...defaultSettings(), ...settings });
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,30 @@ import { defaultSettings } from "../src/main/constants";
|
|||||||
import { overlayLiveUsageCounters } from "../src/main/settings-live-overlay";
|
import { overlayLiveUsageCounters } from "../src/main/settings-live-overlay";
|
||||||
import { getDebridLinkApiKeyId } from "../src/shared/debrid-link-keys";
|
import { getDebridLinkApiKeyId } from "../src/shared/debrid-link-keys";
|
||||||
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||||
|
import { serializeRealDebridApiAccounts } from "../src/shared/real-debrid-accounts";
|
||||||
|
|
||||||
describe("live settings overlay", () => {
|
describe("live settings overlay", () => {
|
||||||
|
it("keeps only live Real-Debrid status and usage for accounts still configured", () => {
|
||||||
|
const keepToken = "fixture-overlay-rd-keep";
|
||||||
|
const removedToken = "fixture-overlay-rd-removed";
|
||||||
|
const keepId = "rda_overlayKeep";
|
||||||
|
const removedId = "rda_overlayRemoved";
|
||||||
|
const target = { ...defaultSettings(), realDebridApiTokens: serializeRealDebridApiAccounts([{ id: keepId, token: keepToken }]) };
|
||||||
|
const status = (accountId: string) => ({ accountId, provider: "realdebrid" as const, label: "Real-Debrid", maskedLogin: "Geschützt", valid: true, isPremium: true, premiumUntilMs: null, message: "Premium aktiv", checkedAt: 1 });
|
||||||
|
const live = {
|
||||||
|
...defaultSettings(),
|
||||||
|
realDebridApiTokens: serializeRealDebridApiAccounts([{ id: keepId, token: keepToken }, { id: removedId, token: removedToken }]),
|
||||||
|
realDebridAccountDailyUsageBytes: { [keepId]: 10, [removedId]: 20 },
|
||||||
|
realDebridAccountTotalUsageBytes: { [keepId]: 100, [removedId]: 200 },
|
||||||
|
debridAccountStatuses: { [keepId]: status(keepId), [removedId]: status(removedId) }
|
||||||
|
};
|
||||||
|
|
||||||
|
overlayLiveUsageCounters(target, live, 1);
|
||||||
|
|
||||||
|
expect(target.realDebridAccountDailyUsageBytes).toEqual({ [keepId]: 10 });
|
||||||
|
expect(target.realDebridAccountTotalUsageBytes).toEqual({ [keepId]: 100 });
|
||||||
|
expect(target.debridAccountStatuses).toEqual({ [keepId]: status(keepId) });
|
||||||
|
});
|
||||||
it("keeps current Mega-Debrid counters and drops data for identities no longer configured", () => {
|
it("keeps current Mega-Debrid counters and drops data for identities no longer configured", () => {
|
||||||
const keepMegaId = getMegaDebridAccountId("keep@example.com");
|
const keepMegaId = getMegaDebridAccountId("keep@example.com");
|
||||||
const removedMegaId = getMegaDebridAccountId("removed@example.com");
|
const removedMegaId = getMegaDebridAccountId("removed@example.com");
|
||||||
|
|||||||
+41
-10
@@ -5,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|||||||
import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys";
|
import { parseDebridLinkApiKeys } from "../src/shared/debrid-link-keys";
|
||||||
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
import { getMegaDebridAccountId } from "../src/shared/mega-debrid-accounts";
|
||||||
import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
|
import { getProviderUsageDayKey } from "../src/shared/provider-daily-limits";
|
||||||
import { getRealDebridApiAccountId } from "../src/shared/real-debrid-accounts";
|
import { parseRealDebridApiAccounts, serializeRealDebridApiAccounts } from "../src/shared/real-debrid-accounts";
|
||||||
import { AppSettings } from "../src/shared/types";
|
import { AppSettings } from "../src/shared/types";
|
||||||
import { defaultSettings } from "../src/main/constants";
|
import { defaultSettings } from "../src/main/constants";
|
||||||
import { configureCredentialProtector } from "../src/main/credential-protection";
|
import { configureCredentialProtector } from "../src/main/credential-protection";
|
||||||
@@ -663,7 +663,6 @@ describe("settings storage", () => {
|
|||||||
it("migrates legacy Real-Debrid API and Web accounts with their existing status", () => {
|
it("migrates legacy Real-Debrid API and Web accounts with their existing status", () => {
|
||||||
const checkedAt = Date.now();
|
const checkedAt = Date.now();
|
||||||
const apiToken = "legacy-real-debrid-token";
|
const apiToken = "legacy-real-debrid-token";
|
||||||
const apiId = getRealDebridApiAccountId(apiToken);
|
|
||||||
const legacyApi = {
|
const legacyApi = {
|
||||||
...defaultSettings(),
|
...defaultSettings(),
|
||||||
token: apiToken,
|
token: apiToken,
|
||||||
@@ -685,8 +684,11 @@ describe("settings storage", () => {
|
|||||||
delete legacyApi.realDebridApiTokens;
|
delete legacyApi.realDebridApiTokens;
|
||||||
delete legacyApi.realDebridWebAccountIds;
|
delete legacyApi.realDebridWebAccountIds;
|
||||||
const normalizedApi = normalizeSettings(legacyApi as AppSettings);
|
const normalizedApi = normalizeSettings(legacyApi as AppSettings);
|
||||||
|
const [migratedApiAccount] = parseRealDebridApiAccounts(normalizedApi.realDebridApiTokens);
|
||||||
|
const apiId = migratedApiAccount.id;
|
||||||
|
|
||||||
expect(normalizedApi.realDebridApiTokens).toBe(apiToken);
|
expect(migratedApiAccount.token).toBe(apiToken);
|
||||||
|
expect(apiId).toMatch(/^rda_[A-Za-z0-9_-]+$/);
|
||||||
expect(normalizedApi.realDebridWebAccountIds).toEqual([]);
|
expect(normalizedApi.realDebridWebAccountIds).toEqual([]);
|
||||||
expect(normalizedApi.debridAccountStatuses[apiId]).toMatchObject({
|
expect(normalizedApi.debridAccountStatuses[apiId]).toMatchObject({
|
||||||
accountId: apiId,
|
accountId: apiId,
|
||||||
@@ -739,9 +741,30 @@ describe("settings storage", () => {
|
|||||||
expect(normalized.realDebridWebAccountIds).toEqual([]);
|
expect(normalized.realDebridWebAccountIds).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps two opaque Real-Debrid API IDs across normalize, save and load", () => {
|
||||||
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rd-store-opaque-rd-"));
|
||||||
|
tempDirs.push(dir);
|
||||||
|
const paths = createStoragePaths(dir);
|
||||||
|
const expected = [
|
||||||
|
{ id: "rda_persistedFirst", token: "persisted-first-token" },
|
||||||
|
{ id: "rda_persistedSecond", token: "persisted-second-token" }
|
||||||
|
];
|
||||||
|
const normalized = normalizeSettings({
|
||||||
|
...defaultSettings(),
|
||||||
|
realDebridApiTokens: serializeRealDebridApiAccounts(expected)
|
||||||
|
});
|
||||||
|
|
||||||
|
saveSettings(paths, normalized);
|
||||||
|
const loaded = loadSettings(paths);
|
||||||
|
|
||||||
|
expect(parseRealDebridApiAccounts(normalized.realDebridApiTokens).map(({ id, token }) => ({ id, token }))).toEqual(expected);
|
||||||
|
expect(parseRealDebridApiAccounts(loaded.realDebridApiTokens).map(({ id, token }) => ({ id, token }))).toEqual(expected);
|
||||||
|
expect(normalizeSettings(loaded)).toEqual(loaded);
|
||||||
|
});
|
||||||
|
|
||||||
it("prefers a concrete Real-Debrid status over the legacy status regardless of object order", () => {
|
it("prefers a concrete Real-Debrid status over the legacy status regardless of object order", () => {
|
||||||
const token = "status-order-token";
|
const token = "status-order-token";
|
||||||
const accountId = getRealDebridApiAccountId(token);
|
const accountId = "rda_statusOrder";
|
||||||
const checkedAt = Date.now();
|
const checkedAt = Date.now();
|
||||||
const legacyStatus = {
|
const legacyStatus = {
|
||||||
accountId: "svc-realdebrid",
|
accountId: "svc-realdebrid",
|
||||||
@@ -767,7 +790,7 @@ describe("settings storage", () => {
|
|||||||
};
|
};
|
||||||
const normalizeWithOrder = (entries: [string, typeof legacyStatus][]) => normalizeSettings({
|
const normalizeWithOrder = (entries: [string, typeof legacyStatus][]) => normalizeSettings({
|
||||||
...defaultSettings(),
|
...defaultSettings(),
|
||||||
realDebridApiTokens: token,
|
realDebridApiTokens: serializeRealDebridApiAccounts([{ id: accountId, token }]),
|
||||||
debridAccountStatuses: Object.fromEntries(entries)
|
debridAccountStatuses: Object.fromEntries(entries)
|
||||||
}).debridAccountStatuses[accountId];
|
}).debridAccountStatuses[accountId];
|
||||||
|
|
||||||
@@ -782,11 +805,16 @@ describe("settings storage", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("normalizes the Real-Debrid pool idempotently and prunes stale account maps", () => {
|
it("normalizes the Real-Debrid pool idempotently and prunes stale account maps", () => {
|
||||||
const apiId = getRealDebridApiAccountId("api-token");
|
const apiId = "rda_apiPrimary";
|
||||||
|
const secondApiId = "rda_apiSecond";
|
||||||
const today = getProviderUsageDayKey();
|
const today = getProviderUsageDayKey();
|
||||||
const once = normalizeSettings({
|
const once = normalizeSettings({
|
||||||
...defaultSettings(),
|
...defaultSettings(),
|
||||||
realDebridApiTokens: "api-token\napi-token\nsecond-token",
|
realDebridApiTokens: serializeRealDebridApiAccounts([
|
||||||
|
{ id: apiId, token: "api-token" },
|
||||||
|
{ id: "rda_duplicate", token: "api-token" },
|
||||||
|
{ id: secondApiId, token: "second-token" }
|
||||||
|
]),
|
||||||
realDebridWebAccountIds: ["rdw_legacy", "rdw_second", "broken", "rdw_second"],
|
realDebridWebAccountIds: ["rdw_legacy", "rdw_second", "broken", "rdw_second"],
|
||||||
realDebridDisabledAccountIds: [apiId, "rdw_second", "stale"],
|
realDebridDisabledAccountIds: [apiId, "rdw_second", "stale"],
|
||||||
realDebridAccountDailyLimitBytes: { [apiId]: 1000, rdw_second: 2000, stale: 3000 },
|
realDebridAccountDailyLimitBytes: { [apiId]: 1000, rdw_second: 2000, stale: 3000 },
|
||||||
@@ -796,7 +824,10 @@ describe("settings storage", () => {
|
|||||||
});
|
});
|
||||||
const twice = normalizeSettings(once);
|
const twice = normalizeSettings(once);
|
||||||
|
|
||||||
expect(once.realDebridApiTokens).toBe("api-token\nsecond-token");
|
expect(parseRealDebridApiAccounts(once.realDebridApiTokens).map((account) => ({ id: account.id, token: account.token }))).toEqual([
|
||||||
|
{ id: apiId, token: "api-token" },
|
||||||
|
{ id: secondApiId, token: "second-token" }
|
||||||
|
]);
|
||||||
expect(once.realDebridWebAccountIds).toEqual(["rdw_legacy", "rdw_second"]);
|
expect(once.realDebridWebAccountIds).toEqual(["rdw_legacy", "rdw_second"]);
|
||||||
expect(once.realDebridDisabledAccountIds).toEqual([apiId, "rdw_second"]);
|
expect(once.realDebridDisabledAccountIds).toEqual([apiId, "rdw_second"]);
|
||||||
expect(once.realDebridAccountDailyLimitBytes).toEqual({ [apiId]: 1000, rdw_second: 2000 });
|
expect(once.realDebridAccountDailyLimitBytes).toEqual({ [apiId]: 1000, rdw_second: 2000 });
|
||||||
@@ -806,10 +837,10 @@ describe("settings storage", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("resets stale per-account Real-Debrid daily usage", () => {
|
it("resets stale per-account Real-Debrid daily usage", () => {
|
||||||
const apiId = getRealDebridApiAccountId("api-token");
|
const apiId = "rda_staleUsage";
|
||||||
const normalized = normalizeSettings({
|
const normalized = normalizeSettings({
|
||||||
...defaultSettings(),
|
...defaultSettings(),
|
||||||
realDebridApiTokens: "api-token",
|
realDebridApiTokens: serializeRealDebridApiAccounts([{ id: apiId, token: "api-token" }]),
|
||||||
providerDailyUsageDay: "2001-01-01",
|
providerDailyUsageDay: "2001-01-01",
|
||||||
realDebridAccountDailyUsageBytes: { [apiId]: 4000 },
|
realDebridAccountDailyUsageBytes: { [apiId]: 4000 },
|
||||||
realDebridAccountTotalUsageBytes: { [apiId]: 9000 }
|
realDebridAccountTotalUsageBytes: { [apiId]: 9000 }
|
||||||
|
|||||||
Reference in New Issue
Block a user