feat(settings): expose Real-Debrid multi-account controls
This commit is contained in:
@@ -81,9 +81,14 @@ function sanitizeSettingsPatch(partial: Partial<AppSettings>): Partial<AppSettin
|
|||||||
return Object.fromEntries(entries) as Partial<AppSettings>;
|
return Object.fromEntries(entries) as Partial<AppSettings>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function settingsFingerprint(settings: AppSettings): string {
|
function settingsFingerprint(settings: AppSettings): string {
|
||||||
return JSON.stringify(normalizeSettings(settings));
|
return JSON.stringify(normalizeSettings(settings));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PendingRealDebridWebAccount = {
|
||||||
|
generation: number;
|
||||||
|
dailyLimitBytes: number;
|
||||||
|
};
|
||||||
|
|
||||||
export class AppController {
|
export class AppController {
|
||||||
private settings: AppSettings;
|
private settings: AppSettings;
|
||||||
@@ -94,7 +99,7 @@ export class AppController {
|
|||||||
|
|
||||||
private realDebridWebFallbacks = new Map<string, RealDebridWebFallback>();
|
private realDebridWebFallbacks = new Map<string, RealDebridWebFallback>();
|
||||||
|
|
||||||
private pendingRealDebridWebAccountIds = new Map<string, number>();
|
private pendingRealDebridWebAccountIds = new Map<string, PendingRealDebridWebAccount>();
|
||||||
|
|
||||||
private realDebridWebGenerations = new Map<string, number>();
|
private realDebridWebGenerations = new Map<string, number>();
|
||||||
|
|
||||||
@@ -732,7 +737,10 @@ export class AppController {
|
|||||||
if (!existing) {
|
if (!existing) {
|
||||||
const generation = (this.realDebridWebGenerations.get(accountId) || 0) + 1;
|
const generation = (this.realDebridWebGenerations.get(accountId) || 0) + 1;
|
||||||
this.realDebridWebGenerations.set(accountId, generation);
|
this.realDebridWebGenerations.set(accountId, generation);
|
||||||
this.pendingRealDebridWebAccountIds.set(accountId, generation);
|
this.pendingRealDebridWebAccountIds.set(accountId, {
|
||||||
|
generation,
|
||||||
|
dailyLimitBytes: request.create ? Math.floor(request.dailyLimitBytes || 0) : 0
|
||||||
|
});
|
||||||
}
|
}
|
||||||
this.audit("INFO", "Real-Debrid Login-Fenster geöffnet", { accountId, create: !existing });
|
this.audit("INFO", "Real-Debrid Login-Fenster geöffnet", { accountId, create: !existing });
|
||||||
try {
|
try {
|
||||||
@@ -789,7 +797,8 @@ export class AppController {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!account) {
|
if (!account) {
|
||||||
if (this.pendingRealDebridWebAccountIds.get(accountId) !== generation) {
|
const pending = this.pendingRealDebridWebAccountIds.get(accountId);
|
||||||
|
if (!pending || pending.generation !== generation) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const applied = applyAccountCommand(this.settings, {
|
const applied = applyAccountCommand(this.settings, {
|
||||||
@@ -797,7 +806,7 @@ export class AppController {
|
|||||||
kind: "realdebrid-web",
|
kind: "realdebrid-web",
|
||||||
identity: accountId,
|
identity: accountId,
|
||||||
secret: "",
|
secret: "",
|
||||||
dailyLimitBytes: 0
|
dailyLimitBytes: pending.dailyLimitBytes
|
||||||
});
|
});
|
||||||
this.pendingRealDebridWebAccountIds.delete(accountId);
|
this.pendingRealDebridWebAccountIds.delete(accountId);
|
||||||
this.updateSettings(applied.settings);
|
this.updateSettings(applied.settings);
|
||||||
|
|||||||
@@ -153,6 +153,10 @@ export function createRendererSettings(settings: AppSettings): RendererSettings
|
|||||||
return {
|
return {
|
||||||
language: settings.language,
|
language: settings.language,
|
||||||
realDebridUseWebLogin: settings.realDebridUseWebLogin,
|
realDebridUseWebLogin: settings.realDebridUseWebLogin,
|
||||||
|
realDebridDisabledAccountIds: [...settings.realDebridDisabledAccountIds],
|
||||||
|
realDebridAccountDailyLimitBytes: { ...settings.realDebridAccountDailyLimitBytes },
|
||||||
|
realDebridAccountDailyUsageBytes: { ...settings.realDebridAccountDailyUsageBytes },
|
||||||
|
realDebridAccountTotalUsageBytes: { ...settings.realDebridAccountTotalUsageBytes },
|
||||||
megaDebridApiEnabled: settings.megaDebridApiEnabled,
|
megaDebridApiEnabled: settings.megaDebridApiEnabled,
|
||||||
megaDebridWebEnabled: settings.megaDebridWebEnabled,
|
megaDebridWebEnabled: settings.megaDebridWebEnabled,
|
||||||
megaDebridPreferApi: settings.megaDebridPreferApi,
|
megaDebridPreferApi: settings.megaDebridPreferApi,
|
||||||
|
|||||||
+139
-38
@@ -1,6 +1,7 @@
|
|||||||
import { DragEvent, ReactElement, memo, useCallback, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
import { DragEvent, ReactElement, memo, useCallback, useDeferredValue, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||||
import { flushSync } from "react-dom";
|
import { flushSync } from "react-dom";
|
||||||
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
||||||
|
import type { RealDebridLoginRequest } from "../shared/preload-api";
|
||||||
import type {
|
import type {
|
||||||
AccountCreateCommand,
|
AccountCreateCommand,
|
||||||
AllDebridHostInfo,
|
AllDebridHostInfo,
|
||||||
@@ -40,7 +41,7 @@ import {
|
|||||||
} from "../shared/provider-daily-limits";
|
} from "../shared/provider-daily-limits";
|
||||||
import { preservePackageOrderForDisplay, sortPackageOrderByName } from "./package-order";
|
import { preservePackageOrderForDisplay, sortPackageOrderByName } from "./package-order";
|
||||||
import { pruneSelection, releaseAccountSelectionFocus, resolveEscapeSelectionScope, shouldClearDownloadSelection } from "./selection";
|
import { pruneSelection, releaseAccountSelectionFocus, resolveEscapeSelectionScope, shouldClearDownloadSelection } from "./selection";
|
||||||
import { buildConfiguredProviderOrder, buildScopedAccountEnabledState, filterAccountDialogOptions, getAccountDialogSelectableOptions, pruneAccountRowSelections, resolveAccountStatusState, resolveAccountUsername, resolveVisibleAccountKind, runOptimisticAccountUpdate, updateAccountRowSelection } from "./account-ui";
|
import { buildConfiguredProviderOrder, buildScopedAccountEnabledState, filterAccountDialogOptions, getAccountDialogSelectableOptions, getAvailableAccountOptions, pruneAccountRowSelections, resolveAccountStatusState, resolveAccountUsername, resolveVisibleAccountKind, runOptimisticAccountUpdate, sortAccountServices, updateAccountRowSelection } from "./account-ui";
|
||||||
import { buildAccountDeleteCommand, buildAccountReplaceCommand, buildAccountSecretRequest, createAccountEditState, validateAccountEdit } from "./account-edit";
|
import { buildAccountDeleteCommand, buildAccountReplaceCommand, buildAccountSecretRequest, createAccountEditState, validateAccountEdit } from "./account-edit";
|
||||||
import type { AccountEditState, AccountEditTarget, AccountKind, AccountService, SingleAccountKind } from "./account-edit";
|
import type { AccountEditState, AccountEditTarget, AccountKind, AccountService, SingleAccountKind } from "./account-edit";
|
||||||
import { ACCOUNT_SERVICE_ICONS } from "./account-service-icons";
|
import { ACCOUNT_SERVICE_ICONS } from "./account-service-icons";
|
||||||
@@ -308,7 +309,7 @@ interface AccountTableRow {
|
|||||||
dailyLimitBytes: number;
|
dailyLimitBytes: number;
|
||||||
dailyRemainingBytes: number;
|
dailyRemainingBytes: number;
|
||||||
totalUsedBytes: number;
|
totalUsedBytes: number;
|
||||||
toggleKind: "mega" | "dl" | "single";
|
toggleKind: "rd" | "mega" | "dl" | "single";
|
||||||
dlKey?: DebridLinkAccountKeyEntry;
|
dlKey?: DebridLinkAccountKeyEntry;
|
||||||
editTarget: AccountEditTarget;
|
editTarget: AccountEditTarget;
|
||||||
}
|
}
|
||||||
@@ -793,6 +794,15 @@ export function buildAccountCreateCommand(dialog: AccountDialogState): AccountCr
|
|||||||
dailyLimitBytes: parseAccountDailyLimitInputBytes(dialog.dailyLimitGb) || 0
|
dailyLimitBytes: parseAccountDailyLimitInputBytes(dialog.dailyLimitGb) || 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function buildRealDebridWebCreateLoginRequest(dialog: AccountDialogState, accountId: string): RealDebridLoginRequest | null {
|
||||||
|
if (dialog.kind !== "realdebrid-web") return null;
|
||||||
|
return {
|
||||||
|
accountId,
|
||||||
|
create: true,
|
||||||
|
dailyLimitBytes: parseAccountDailyLimitInputBytes(dialog.dailyLimitGb) || 0
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const emptyStats = (): DownloadStats => ({
|
const emptyStats = (): DownloadStats => ({
|
||||||
totalDownloaded: 0,
|
totalDownloaded: 0,
|
||||||
@@ -810,7 +820,7 @@ const emptyStats = (): DownloadStats => ({
|
|||||||
|
|
||||||
const emptySnapshot = (): UiSnapshot => ({
|
const emptySnapshot = (): UiSnapshot => ({
|
||||||
settings: {
|
settings: {
|
||||||
language: "en", realDebridUseWebLogin: false, megaDebridApiEnabled: false, megaDebridWebEnabled: false, megaDebridPreferApi: true, bestDebridUseWebLogin: false, allDebridUseWebLogin: false,
|
language: "en", realDebridUseWebLogin: false, realDebridDisabledAccountIds: [], realDebridAccountDailyLimitBytes: {}, realDebridAccountDailyUsageBytes: {}, realDebridAccountTotalUsageBytes: {}, megaDebridApiEnabled: false, megaDebridWebEnabled: false, megaDebridPreferApi: true, bestDebridUseWebLogin: false, allDebridUseWebLogin: false,
|
||||||
debridLinkDisabledKeyIds: [],
|
debridLinkDisabledKeyIds: [],
|
||||||
archivePasswordListConfigured: false, notifyUrlConfigured: false,
|
archivePasswordListConfigured: false, notifyUrlConfigured: false,
|
||||||
rememberToken: true, configuredProviders: [], providerOrder: [], providerPrimary: "realdebrid", providerSecondary: "none",
|
rememberToken: true, configuredProviders: [], providerOrder: [], providerPrimary: "realdebrid", providerSecondary: "none",
|
||||||
@@ -2307,8 +2317,55 @@ export function App(): ReactElement {
|
|||||||
|
|
||||||
const accountRows = useMemo(() => {
|
const accountRows = useMemo(() => {
|
||||||
const rows: AccountTableRow[] = [];
|
const rows: AccountTableRow[] = [];
|
||||||
for (const entry of configuredAccounts) {
|
for (const entry of configuredAccounts) {
|
||||||
if (entry.kind === "megadebrid-api" || entry.kind === "megadebrid-web") {
|
if (entry.service === "realdebrid") {
|
||||||
|
const accounts = snapshot.accounts.filter((account): account is RendererAccount & { kind: "realdebrid-api" | "realdebrid-web" } => (
|
||||||
|
account.provider === "realdebrid" && (account.kind === "realdebrid-api" || account.kind === "realdebrid-web")
|
||||||
|
));
|
||||||
|
for (const account of accounts) {
|
||||||
|
const option = findAccountOption(account.kind);
|
||||||
|
const disabled = (settingsDraft.disabledProviders || []).includes("realdebrid")
|
||||||
|
|| (settingsDraft.realDebridDisabledAccountIds || []).includes(account.accountId);
|
||||||
|
const rowEntry: ConfiguredAccountEntry = {
|
||||||
|
...entry,
|
||||||
|
kind: account.kind,
|
||||||
|
modeLabel: option.modeLabel,
|
||||||
|
statusLabel: disabled ? "Deaktiviert" : "Aktiviert",
|
||||||
|
summary: account.maskedIdentity,
|
||||||
|
summaryLines: [account.maskedIdentity],
|
||||||
|
disabled,
|
||||||
|
dailyUsedBytes: account.dailyUsageBytes,
|
||||||
|
totalUsedBytes: account.totalUsageBytes,
|
||||||
|
dailyLimitBytes: account.dailyLimitBytes,
|
||||||
|
dailyRemainingBytes: account.dailyLimitBytes > 0 ? Math.max(0, account.dailyLimitBytes - account.dailyUsageBytes) : 0,
|
||||||
|
dailyLimitReached: account.dailyLimitBytes > 0 && account.dailyUsageBytes >= account.dailyLimitBytes
|
||||||
|
};
|
||||||
|
rows.push({
|
||||||
|
rowKey: `rd-${account.accountId}`,
|
||||||
|
entry: rowEntry,
|
||||||
|
hosterLabel: option.serviceLabel,
|
||||||
|
modeLabel: option.modeLabel,
|
||||||
|
username: account.identity,
|
||||||
|
credentialLabel: getAccountCredentialLabel(account.kind),
|
||||||
|
accountId: account.accountId,
|
||||||
|
checkable: true,
|
||||||
|
disabled,
|
||||||
|
dailyUsedBytes: account.dailyUsageBytes,
|
||||||
|
dailyLimitBytes: account.dailyLimitBytes,
|
||||||
|
dailyRemainingBytes: account.dailyLimitBytes > 0 ? Math.max(0, account.dailyLimitBytes - account.dailyUsageBytes) : 0,
|
||||||
|
totalUsedBytes: account.totalUsageBytes,
|
||||||
|
toggleKind: "rd",
|
||||||
|
editTarget: {
|
||||||
|
type: "single",
|
||||||
|
rowKey: `rd-${account.accountId}`,
|
||||||
|
kind: account.kind,
|
||||||
|
service: "realdebrid",
|
||||||
|
provider: "realdebrid",
|
||||||
|
accountId: account.accountId
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (entry.kind === "megadebrid-api" || entry.kind === "megadebrid-web") {
|
||||||
const accounts = accountsOfKind(entry.kind, snapshot.accounts);
|
const accounts = accountsOfKind(entry.kind, snapshot.accounts);
|
||||||
for (const acc of accounts) {
|
for (const acc of accounts) {
|
||||||
const used = acc.dailyUsageBytes;
|
const used = acc.dailyUsageBytes;
|
||||||
@@ -2367,7 +2424,7 @@ export function App(): ReactElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const serviceAccountId = entry.service === "realdebrid" ? "svc-realdebrid" : null;
|
const serviceAccountId = null;
|
||||||
rows.push({
|
rows.push({
|
||||||
rowKey: `svc-${entry.service}`,
|
rowKey: `svc-${entry.service}`,
|
||||||
entry,
|
entry,
|
||||||
@@ -2410,10 +2467,7 @@ export function App(): ReactElement {
|
|||||||
});
|
});
|
||||||
}, [accountRows]);
|
}, [accountRows]);
|
||||||
const availableAccountOptions = useMemo(() => (
|
const availableAccountOptions = useMemo(() => (
|
||||||
ACCOUNT_OPTIONS.filter((option) => option.kind === "megadebrid-api"
|
getAvailableAccountOptions(ACCOUNT_OPTIONS, [...configuredAccountServices])
|
||||||
|| option.kind === "megadebrid-web"
|
|
||||||
|| option.kind === "debridlink-api"
|
|
||||||
|| !configuredAccountServices.has(option.service))
|
|
||||||
), [configuredAccountServices]);
|
), [configuredAccountServices]);
|
||||||
const accountEditOption = accountEditDialog ? findAccountOption(accountEditDialog.target.kind) : null;
|
const accountEditOption = accountEditDialog ? findAccountOption(accountEditDialog.target.kind) : null;
|
||||||
const accountEditRow = accountEditDialog ? accountRows.find((row) => row.rowKey === accountEditDialog.target.rowKey) ?? null : null;
|
const accountEditRow = accountEditDialog ? accountRows.find((row) => row.rowKey === accountEditDialog.target.rowKey) ?? null : null;
|
||||||
@@ -2432,7 +2486,7 @@ export function App(): ReactElement {
|
|||||||
);
|
);
|
||||||
}, [accountDialog, availableAccountOptions]);
|
}, [accountDialog, availableAccountOptions]);
|
||||||
const accountDialogServiceFilters = useMemo(() => (
|
const accountDialogServiceFilters = useMemo(() => (
|
||||||
[...new Set(accountDialogSelectableOptions.map((option) => option.serviceLabel))]
|
sortAccountServices(accountDialogSelectableOptions.map((option) => option.serviceLabel))
|
||||||
), [accountDialogSelectableOptions]);
|
), [accountDialogSelectableOptions]);
|
||||||
const filteredAccountDialogOptions = useMemo(() => (
|
const filteredAccountDialogOptions = useMemo(() => (
|
||||||
filterAccountDialogOptions(accountDialogSelectableOptions, accountDialogSearch, accountDialogServiceFilter)
|
filterAccountDialogOptions(accountDialogSelectableOptions, accountDialogSearch, accountDialogServiceFilter)
|
||||||
@@ -2625,11 +2679,12 @@ export function App(): ReactElement {
|
|||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const runAccountQuickAction = async (action: AccountQuickAction): Promise<void> => {
|
const runAccountQuickAction = async (action: AccountQuickAction, accountId?: string | null): Promise<void> => {
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case "realdebrid-login":
|
case "realdebrid-login":
|
||||||
await window.rd.openRealDebridLogin();
|
if (!accountId) throw new Error("Der ausgewählte Real-Debrid-Account wurde nicht gefunden.");
|
||||||
showToast("Real-Debrid Login-Fenster geöffnet", 2200);
|
await window.rd.openRealDebridLogin({ accountId });
|
||||||
|
showToast("Real-Debrid Login-Fenster geöffnet", 2200);
|
||||||
return;
|
return;
|
||||||
case "bestdebrid-cookies": {
|
case "bestdebrid-cookies": {
|
||||||
const count = await window.rd.importBestDebridCookies();
|
const count = await window.rd.importBestDebridCookies();
|
||||||
@@ -2733,7 +2788,7 @@ export function App(): ReactElement {
|
|||||||
applyPersistedSettings(result.settings);
|
applyPersistedSettings(result.settings);
|
||||||
closeAccountEditDialog();
|
closeAccountEditDialog();
|
||||||
if (quickAction) {
|
if (quickAction) {
|
||||||
await runAccountQuickAction(quickAction);
|
await runAccountQuickAction(quickAction, editSnapshot.target.type === "single" ? editSnapshot.target.accountId : null);
|
||||||
} else {
|
} else {
|
||||||
showToast(`${findAccountOption(editSnapshot.target.kind).title} gespeichert`, 2200);
|
showToast(`${findAccountOption(editSnapshot.target.kind).title} gespeichert`, 2200);
|
||||||
}
|
}
|
||||||
@@ -2758,14 +2813,23 @@ export function App(): ReactElement {
|
|||||||
}
|
}
|
||||||
const selectedOption = dialogSnapshot.kind ? findAccountOption(dialogSnapshot.kind) : null;
|
const selectedOption = dialogSnapshot.kind ? findAccountOption(dialogSnapshot.kind) : null;
|
||||||
await performQuickAction(async () => {
|
await performQuickAction(async () => {
|
||||||
|
if (dialogSnapshot.kind === "realdebrid-web") {
|
||||||
|
const accountId = `rdw_${crypto.randomUUID().replace(/-/g, "")}`;
|
||||||
|
const request = buildRealDebridWebCreateLoginRequest(dialogSnapshot, accountId);
|
||||||
|
if (!request) throw new Error("Account-Payload ist ungültig");
|
||||||
|
await window.rd.openRealDebridLogin(request);
|
||||||
|
closeAccountDialog();
|
||||||
|
showToast("Real-Debrid Login-Fenster geöffnet", 2200);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const command = buildAccountCreateCommand(dialogSnapshot);
|
const command = buildAccountCreateCommand(dialogSnapshot);
|
||||||
if (!command) throw new Error("Account-Payload ist ungültig");
|
if (!command) throw new Error("Account-Payload ist ungültig");
|
||||||
const result = await window.rd.createAccount(command);
|
const result = await window.rd.createAccount(command);
|
||||||
setSnapshot((current) => ({ ...current, settings: result.settings, accounts: result.accounts }));
|
setSnapshot((current) => ({ ...current, settings: result.settings, accounts: result.accounts }));
|
||||||
applyPersistedSettings(result.settings);
|
applyPersistedSettings(result.settings);
|
||||||
closeAccountDialog();
|
closeAccountDialog();
|
||||||
if (quickAction) {
|
if (quickAction) {
|
||||||
await runAccountQuickAction(quickAction);
|
await runAccountQuickAction(quickAction, result.accountId);
|
||||||
} else if (selectedOption) {
|
} else if (selectedOption) {
|
||||||
showToast(`${selectedOption.title} gespeichert`, 2200);
|
showToast(`${selectedOption.title} gespeichert`, 2200);
|
||||||
}
|
}
|
||||||
@@ -2845,15 +2909,15 @@ export function App(): ReactElement {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAccountRowQuickAction = async (entry: ConfiguredAccountEntry): Promise<void> => {
|
const onAccountRowQuickAction = async (row: AccountTableRow): Promise<void> => {
|
||||||
const meta = getAccountQuickActionMeta(entry.kind);
|
const meta = getAccountQuickActionMeta(row.entry.kind);
|
||||||
if (!meta) {
|
if (!meta) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
await performQuickAction(async () => {
|
await performQuickAction(async () => {
|
||||||
await runAccountQuickAction(meta.action);
|
await runAccountQuickAction(meta.action, row.accountId);
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
showToast(`${entry.serviceLabel}: Aktion fehlgeschlagen: ${String(error)}`, 3200);
|
showToast(`${row.entry.serviceLabel}: Aktion fehlgeschlagen: ${String(error)}`, 3200);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2920,9 +2984,31 @@ export function App(): ReactElement {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onToggleRealDebridAccountEnabled = async (accountId: string, enabled: boolean): Promise<void> => {
|
||||||
|
await performQuickAction(async () => {
|
||||||
|
const nextState = buildScopedAccountEnabledState(
|
||||||
|
settingsDraft.disabledProviders || [],
|
||||||
|
["realdebrid"],
|
||||||
|
settingsDraft.realDebridDisabledAccountIds || [],
|
||||||
|
accountId,
|
||||||
|
enabled
|
||||||
|
);
|
||||||
|
await persistAccountToggle({
|
||||||
|
...settingsDraft,
|
||||||
|
disabledProviders: nextState.disabledProviders,
|
||||||
|
realDebridDisabledAccountIds: nextState.disabledAccountIds
|
||||||
|
});
|
||||||
|
showToast(enabled ? "Account aktiviert" : "Account deaktiviert", 2000);
|
||||||
|
}, (error) => {
|
||||||
|
showToast(`Umschalten fehlgeschlagen: ${String(error)}`, 3200);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const toggleAccountTableRow = (row: AccountTableRow): void => {
|
const toggleAccountTableRow = (row: AccountTableRow): void => {
|
||||||
setAccountContextMenu(null);
|
setAccountContextMenu(null);
|
||||||
if (row.toggleKind === "mega" && row.accountId) {
|
if (row.toggleKind === "rd" && row.accountId) {
|
||||||
|
void onToggleRealDebridAccountEnabled(row.accountId, row.disabled);
|
||||||
|
} else if (row.toggleKind === "mega" && row.accountId) {
|
||||||
void onToggleMegaAccountEnabled(row.entry.kind as "megadebrid-api" | "megadebrid-web", row.accountId, row.disabled);
|
void onToggleMegaAccountEnabled(row.entry.kind as "megadebrid-api" | "megadebrid-web", row.accountId, row.disabled);
|
||||||
} else if (row.toggleKind === "dl" && row.dlKey) {
|
} else if (row.toggleKind === "dl" && row.dlKey) {
|
||||||
void onToggleDebridLinkApiKeyEnabled(row.entry, row.dlKey, row.disabled);
|
void onToggleDebridLinkApiKeyEnabled(row.entry, row.dlKey, row.disabled);
|
||||||
@@ -2975,12 +3061,21 @@ export function App(): ReactElement {
|
|||||||
|
|
||||||
const checkAccountTableRow = (row: AccountTableRow): void => {
|
const checkAccountTableRow = (row: AccountTableRow): void => {
|
||||||
setAccountContextMenu(null);
|
setAccountContextMenu(null);
|
||||||
|
if ((row.entry.kind === "realdebrid-api" || row.entry.kind === "realdebrid-web") && row.accountId) {
|
||||||
|
const kind = row.entry.kind;
|
||||||
|
const accountId = row.accountId;
|
||||||
|
void performQuickAction(async () => {
|
||||||
|
const status = await window.rd.checkAccountCredentials({ kind, accountId });
|
||||||
|
showToast(status.valid ? "Account erfolgreich geprüft" : status.message || "Zugangsdaten ungültig", 2600);
|
||||||
|
}, (error) => showToast(`Prüfung fehlgeschlagen: ${String(error)}`, 3200));
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (row.checkable) {
|
if (row.checkable) {
|
||||||
void checkAccounts("all");
|
void checkAccounts("all");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (getAccountQuickActionMeta(row.entry.kind)) {
|
if (getAccountQuickActionMeta(row.entry.kind)) {
|
||||||
void onAccountRowQuickAction(row.entry);
|
void onAccountRowQuickAction(row);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
showToast("Für diesen Account ist keine direkte Statusprüfung verfügbar.", 2800);
|
showToast("Für diesen Account ist keine direkte Statusprüfung verfügbar.", 2800);
|
||||||
@@ -5161,7 +5256,7 @@ export function App(): ReactElement {
|
|||||||
description: option.pickerDescription,
|
description: option.pickerDescription,
|
||||||
functionLabel: getAccountPickerFunctionLabel(option),
|
functionLabel: getAccountPickerFunctionLabel(option),
|
||||||
filter: option.modeLabel === "API" ? "api" : "web",
|
filter: option.modeLabel === "API" ? "api" : "web",
|
||||||
multi: option.kind === "megadebrid-api" || option.kind === "megadebrid-web" || option.kind === "debridlink-api",
|
multi: option.kind === "realdebrid-api" || option.kind === "realdebrid-web" || option.kind === "megadebrid-api" || option.kind === "megadebrid-web" || option.kind === "debridlink-api",
|
||||||
icon: ACCOUNT_SERVICE_ICONS[option.service]
|
icon: ACCOUNT_SERVICE_ICONS[option.service]
|
||||||
}));
|
}));
|
||||||
const accountAddFields = buildAccountAddFields(accountDialog);
|
const accountAddFields = buildAccountAddFields(accountDialog);
|
||||||
@@ -5278,11 +5373,17 @@ export function App(): ReactElement {
|
|||||||
}
|
}
|
||||||
const editSnapshot = accountEditDialog;
|
const editSnapshot = accountEditDialog;
|
||||||
void performQuickAction(async () => {
|
void performQuickAction(async () => {
|
||||||
if (editSnapshot.target.type === "mega" || editSnapshot.target.type === "debridlink") {
|
if (editSnapshot.target.type === "mega" || editSnapshot.target.type === "debridlink"
|
||||||
|
|| editSnapshot.target.kind === "realdebrid-api" || editSnapshot.target.kind === "realdebrid-web") {
|
||||||
const secret = editSnapshot.target.type === "mega" ? editSnapshot.password : editSnapshot.token;
|
const secret = editSnapshot.target.type === "mega" ? editSnapshot.password : editSnapshot.token;
|
||||||
|
const kind = editSnapshot.target.kind as "realdebrid-api" | "realdebrid-web" | "megadebrid-api" | "megadebrid-web" | "debridlink-api";
|
||||||
const status = await window.rd.checkAccountCredentials({
|
const status = await window.rd.checkAccountCredentials({
|
||||||
kind: editSnapshot.target.kind,
|
kind,
|
||||||
accountId: editSnapshot.target.type === "mega" ? editSnapshot.target.accountId : editSnapshot.target.keyId,
|
accountId: editSnapshot.target.type === "mega"
|
||||||
|
? editSnapshot.target.accountId
|
||||||
|
: editSnapshot.target.type === "debridlink"
|
||||||
|
? editSnapshot.target.keyId
|
||||||
|
: editSnapshot.target.accountId,
|
||||||
identity: secret ? editSnapshot.login : undefined,
|
identity: secret ? editSnapshot.login : undefined,
|
||||||
secret: secret || undefined
|
secret: secret || undefined
|
||||||
});
|
});
|
||||||
@@ -5292,7 +5393,7 @@ export function App(): ReactElement {
|
|||||||
}
|
}
|
||||||
const quickAction = getAccountQuickActionMeta(editSnapshot.target.kind);
|
const quickAction = getAccountQuickActionMeta(editSnapshot.target.kind);
|
||||||
if (quickAction) {
|
if (quickAction) {
|
||||||
await runAccountQuickAction(quickAction.action);
|
await runAccountQuickAction(quickAction.action, editSnapshot.target.type === "single" ? editSnapshot.target.accountId : null);
|
||||||
} else {
|
} else {
|
||||||
showToast("Für diesen Dienst ist keine direkte Statusprüfung verfügbar.", 2800);
|
showToast("Für diesen Dienst ist keine direkte Statusprüfung verfügbar.", 2800);
|
||||||
}
|
}
|
||||||
@@ -5977,7 +6078,7 @@ export function App(): ReactElement {
|
|||||||
Account prüfen
|
Account prüfen
|
||||||
</button>
|
</button>
|
||||||
{getAccountQuickActionMeta(activeAccountContextRow.entry.kind) && (
|
{getAccountQuickActionMeta(activeAccountContextRow.entry.kind) && (
|
||||||
<button className="ctx-menu-item" onClick={() => { setAccountContextMenu(null); void onAccountRowQuickAction(activeAccountContextRow.entry); }}>
|
<button className="ctx-menu-item" onClick={() => { setAccountContextMenu(null); void onAccountRowQuickAction(activeAccountContextRow); }}>
|
||||||
{getAccountQuickActionMeta(activeAccountContextRow.entry.kind)?.label}
|
{getAccountQuickActionMeta(activeAccountContextRow.entry.kind)?.label}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -6,6 +6,22 @@ export interface AccountModeOption {
|
|||||||
modeLabel: string;
|
modeLabel: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function sortAccountServices(labels: readonly string[]): string[] {
|
||||||
|
return [...new Set(labels)].sort((left, right) => left.localeCompare(right, "de-DE", { sensitivity: "base" }));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAvailableAccountOptions<T extends { service: string }>(
|
||||||
|
options: readonly T[],
|
||||||
|
configuredServices: readonly string[]
|
||||||
|
): T[] {
|
||||||
|
const configured = new Set(configuredServices);
|
||||||
|
return options.filter((option) => option.service === "realdebrid"
|
||||||
|
|| option.service === "megadebrid-api"
|
||||||
|
|| option.service === "megadebrid-web"
|
||||||
|
|| option.service === "debridlink"
|
||||||
|
|| !configured.has(option.service));
|
||||||
|
}
|
||||||
|
|
||||||
export function matchesAccountModeFilter(option: AccountModeOption, filter: AccountModeFilter): boolean {
|
export function matchesAccountModeFilter(option: AccountModeOption, filter: AccountModeFilter): boolean {
|
||||||
if (filter === "all") {
|
if (filter === "all") {
|
||||||
return true;
|
return true;
|
||||||
@@ -34,7 +50,8 @@ export function filterAccountDialogOptions<T extends {
|
|||||||
.join(" ")
|
.join(" ")
|
||||||
.toLocaleLowerCase("de-DE")
|
.toLocaleLowerCase("de-DE")
|
||||||
.includes(normalizedQuery);
|
.includes(normalizedQuery);
|
||||||
});
|
}).sort((left, right) => left.serviceLabel.localeCompare(right.serviceLabel, "de-DE", { sensitivity: "base" })
|
||||||
|
|| left.title.localeCompare(right.title, "de-DE", { sensitivity: "base" }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildConfiguredProviderOrder(
|
export function buildConfiguredProviderOrder(
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ const pairs = [
|
|||||||
["Keine eigenen Zuordnungen.", "No custom assignments."], ["Hoster-Routing hinzufügen", "Add hoster routing"], ["Hoster hinzufügen…", "Add hoster…"], ["Eigener Hoster…", "Custom hoster…"], ["Noch keine Rotations-Ereignisse.", "No rotation events yet."],
|
["Keine eigenen Zuordnungen.", "No custom assignments."], ["Hoster-Routing hinzufügen", "Add hoster routing"], ["Hoster hinzufügen…", "Add hoster…"], ["Eigener Hoster…", "Custom hoster…"], ["Noch keine Rotations-Ereignisse.", "No rotation events yet."],
|
||||||
["Prüfen und speichern", "Check and save"], ["Wähle einen Dienst und trage die passenden Zugangsdaten ein.", "Choose a service and enter the matching credentials."], ["Accounts durchsuchen", "Search accounts"],
|
["Prüfen und speichern", "Check and save"], ["Wähle einen Dienst und trage die passenden Zugangsdaten ein.", "Choose a service and enter the matching credentials."], ["Accounts durchsuchen", "Search accounts"],
|
||||||
["Dienst / Zugangstyp", "Service / access type"], ["Dienst", "Service"], ["Typ/Funktion", "Type/function"], ["Dienst oder Zugangstyp suchen", "Search service or access type"], ["Zugangstyp suchen, z. B. API oder Web", "Search access type, e.g. API or web"], ["Dienst filtern", "Filter service"], ["Alle Dienste", "All services"], ["Account-Typ filtern", "Filter account type"], ["Verfügbare Account-Typen", "Available account types"], ["Keine passenden Account-Typen.", "No matching account types."],
|
["Dienst / Zugangstyp", "Service / access type"], ["Dienst", "Service"], ["Typ/Funktion", "Type/function"], ["Dienst oder Zugangstyp suchen", "Search service or access type"], ["Zugangstyp suchen, z. B. API oder Web", "Search access type, e.g. API or web"], ["Dienst filtern", "Filter service"], ["Alle Dienste", "All services"], ["Account-Typ filtern", "Filter account type"], ["Verfügbare Account-Typen", "Available account types"], ["Keine passenden Account-Typen.", "No matching account types."],
|
||||||
["Prüfen", "Check"], ["Bearbeite ausschließlich den ausgewählten Account.", "Edit only the selected account."], ["Account bearbeiten", "Edit account"], ["Account aktiviert", "Account enabled"],
|
["Prüfen", "Check"], ["Bearbeite ausschließlich den ausgewählten Account.", "Edit only the selected account."], ["Account bearbeiten", "Edit account"], ["Account aktiviert", "Account enabled"], ["Der ausgewählte Real-Debrid-Account wurde nicht gefunden.", "The selected Real-Debrid account was not found."],
|
||||||
["Immer erste Tonspur", "Always first audio track"], ["Pro Download", "Per download"], ["Keine Archive löschen", "Do not delete archives"], ["Archive in Papierkorb", "Move archives to recycle bin"], ["Archive löschen", "Delete archives"],
|
["Immer erste Tonspur", "Always first audio track"], ["Pro Download", "Per download"], ["Keine Archive löschen", "Do not delete archives"], ["Archive in Papierkorb", "Move archives to recycle bin"], ["Archive löschen", "Delete archives"],
|
||||||
["Accounts und Verwendungsregeln.", "Accounts and usage rules."], ["Premium Account", "Premium account"], ["Zugang ungültig", "Invalid access"], ["Prüft…", "Checking…"], ["Geschützter Zugang", "Protected access"],
|
["Accounts und Verwendungsregeln.", "Accounts and usage rules."], ["Premium Account", "Premium account"], ["Zugang ungültig", "Invalid access"], ["Prüft…", "Checking…"], ["Geschützter Zugang", "Protected access"],
|
||||||
["Der ausgewählte Mega-Debrid-Account wurde nicht gefunden.", "The selected Mega-Debrid account was not found."], ["Der ausgewählte Debrid-Link-Key wurde nicht gefunden.", "The selected Debrid-Link key was not found."],
|
["Der ausgewählte Mega-Debrid-Account wurde nicht gefunden.", "The selected Mega-Debrid account was not found."], ["Der ausgewählte Debrid-Link-Key wurde nicht gefunden.", "The selected Debrid-Link key was not found."],
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ import { isRealDebridWebAccountId } from "./real-debrid-accounts";
|
|||||||
export interface RealDebridLoginRequest {
|
export interface RealDebridLoginRequest {
|
||||||
accountId: string;
|
accountId: string;
|
||||||
create?: boolean;
|
create?: boolean;
|
||||||
|
dailyLimitBytes?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function validateRealDebridLoginRequest(value: unknown): Required<RealDebridLoginRequest> {
|
export function validateRealDebridLoginRequest(value: unknown): Required<RealDebridLoginRequest> {
|
||||||
@@ -44,13 +45,22 @@ export function validateRealDebridLoginRequest(value: unknown): Required<RealDeb
|
|||||||
throw new Error("Account-Payload ist ungültig");
|
throw new Error("Account-Payload ist ungültig");
|
||||||
}
|
}
|
||||||
const raw = value as Record<string, unknown>;
|
const raw = value as Record<string, unknown>;
|
||||||
if (Object.keys(raw).some((key) => key !== "accountId" && key !== "create")
|
if (Object.keys(raw).some((key) => key !== "accountId" && key !== "create" && key !== "dailyLimitBytes")
|
||||||
|| typeof raw.accountId !== "string"
|
|| typeof raw.accountId !== "string"
|
||||||
|| !isRealDebridWebAccountId(raw.accountId)
|
|| !isRealDebridWebAccountId(raw.accountId)
|
||||||
|| (raw.create !== undefined && typeof raw.create !== "boolean")) {
|
|| (raw.create !== undefined && typeof raw.create !== "boolean")
|
||||||
|
|| (raw.dailyLimitBytes !== undefined && (typeof raw.dailyLimitBytes !== "number"
|
||||||
|
|| !Number.isFinite(raw.dailyLimitBytes)
|
||||||
|
|| raw.dailyLimitBytes < 0
|
||||||
|
|| raw.dailyLimitBytes > Number.MAX_SAFE_INTEGER))) {
|
||||||
throw new Error("Account-Payload ist ungültig");
|
throw new Error("Account-Payload ist ungültig");
|
||||||
}
|
}
|
||||||
return { accountId: raw.accountId.trim(), create: raw.create === true };
|
const create = raw.create === true;
|
||||||
|
const dailyLimitBytes = Math.floor(Number(raw.dailyLimitBytes) || 0);
|
||||||
|
if (!create && dailyLimitBytes !== 0) {
|
||||||
|
throw new Error("Account-Payload ist ungültig");
|
||||||
|
}
|
||||||
|
return { accountId: raw.accountId.trim(), create, dailyLimitBytes: create ? dailyLimitBytes : 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ElectronApi {
|
export interface ElectronApi {
|
||||||
|
|||||||
@@ -237,6 +237,10 @@ export interface RendererAccount {
|
|||||||
export interface RendererSettings {
|
export interface RendererSettings {
|
||||||
language: AppLanguage;
|
language: AppLanguage;
|
||||||
realDebridUseWebLogin: boolean;
|
realDebridUseWebLogin: boolean;
|
||||||
|
realDebridDisabledAccountIds: string[];
|
||||||
|
realDebridAccountDailyLimitBytes: Record<string, number>;
|
||||||
|
realDebridAccountDailyUsageBytes: Record<string, number>;
|
||||||
|
realDebridAccountTotalUsageBytes: Record<string, number>;
|
||||||
megaDebridApiEnabled: boolean;
|
megaDebridApiEnabled: boolean;
|
||||||
megaDebridWebEnabled: boolean;
|
megaDebridWebEnabled: boolean;
|
||||||
megaDebridPreferApi: boolean;
|
megaDebridPreferApi: boolean;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { buildAccountCreateCommand, createAccountDialogState } from "../src/renderer/App";
|
import { buildAccountCreateCommand, buildRealDebridWebCreateLoginRequest, createAccountDialogState } from "../src/renderer/App";
|
||||||
import { createRendererSettings } from "../src/main/renderer-state";
|
import { createRendererSettings } from "../src/main/renderer-state";
|
||||||
import { defaultSettings } from "../src/main/constants";
|
import { defaultSettings } from "../src/main/constants";
|
||||||
|
|
||||||
@@ -27,4 +27,18 @@ describe("account creation dialog", () => {
|
|||||||
expect(dialog.password).toBe("");
|
expect(dialog.password).toBe("");
|
||||||
expect(dialog.megaAccounts).toEqual([]);
|
expect(dialog.megaAccounts).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("forwards the selected daily limit with a newly reserved Real-Debrid Web account", () => {
|
||||||
|
const settings = createRendererSettings(defaultSettings());
|
||||||
|
const dialog = {
|
||||||
|
...createAccountDialogState("create", "realdebrid-web", settings),
|
||||||
|
dailyLimitGb: "2,5"
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(buildRealDebridWebCreateLoginRequest(dialog, "rdw_reservedopaqueid")).toEqual({
|
||||||
|
accountId: "rdw_reservedopaqueid",
|
||||||
|
create: true,
|
||||||
|
dailyLimitBytes: Math.floor(2.5 * 1024 * 1024 * 1024)
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -22,6 +22,39 @@ function megaTarget(identity: string): AccountEditTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("renderer-safe account editing", () => {
|
describe("renderer-safe account editing", () => {
|
||||||
|
it("targets one concrete Real-Debrid API account for replace and delete", () => {
|
||||||
|
const settings = defaultSettings();
|
||||||
|
settings.realDebridApiTokens = JSON.stringify({
|
||||||
|
version: 1,
|
||||||
|
accounts: [
|
||||||
|
{ id: "rda_first", token: "fixture-token-first" },
|
||||||
|
{ id: "rda_second", token: "fixture-token-second" }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
const renderer = createRendererState(settings);
|
||||||
|
const target: AccountEditTarget = {
|
||||||
|
type: "single",
|
||||||
|
rowKey: "rd-rda_second",
|
||||||
|
kind: "realdebrid-api",
|
||||||
|
service: "realdebrid",
|
||||||
|
provider: "realdebrid",
|
||||||
|
accountId: "rda_second"
|
||||||
|
};
|
||||||
|
const edit = createAccountEditState(target, renderer.accounts);
|
||||||
|
|
||||||
|
expect(buildAccountReplaceCommand({ ...edit, token: "replacement-token" })).toEqual(expect.objectContaining({
|
||||||
|
action: "replace",
|
||||||
|
kind: "realdebrid-api",
|
||||||
|
accountId: "rda_second",
|
||||||
|
secret: "replacement-token"
|
||||||
|
}));
|
||||||
|
expect(buildAccountDeleteCommand(target)).toEqual({
|
||||||
|
action: "delete",
|
||||||
|
kind: "realdebrid-api",
|
||||||
|
accountId: "rda_second"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("opens an existing account without reading its stored secret", () => {
|
it("opens an existing account without reading its stored secret", () => {
|
||||||
const identity = "safe-edit@example.test";
|
const identity = "safe-edit@example.test";
|
||||||
const state = createRendererState({
|
const state = createRendererState({
|
||||||
|
|||||||
@@ -76,11 +76,11 @@ describe("account preload contract", () => {
|
|||||||
|
|
||||||
it("forwards account-bound existing and create browser logins", async () => {
|
it("forwards account-bound existing and create browser logins", async () => {
|
||||||
await electron.api?.openRealDebridLogin({ accountId: "rdw_existing" });
|
await electron.api?.openRealDebridLogin({ accountId: "rdw_existing" });
|
||||||
await electron.api?.openRealDebridLogin({ accountId: "rdw_reserved", create: true });
|
await electron.api?.openRealDebridLogin({ accountId: "rdw_reserved", create: true, dailyLimitBytes: 10_000 });
|
||||||
|
|
||||||
expect(electron.invoke.mock.calls).toEqual([
|
expect(electron.invoke.mock.calls).toEqual([
|
||||||
[IPC_CHANNELS.OPEN_REALDEBRID_LOGIN, { accountId: "rdw_existing" }],
|
[IPC_CHANNELS.OPEN_REALDEBRID_LOGIN, { accountId: "rdw_existing" }],
|
||||||
[IPC_CHANNELS.OPEN_REALDEBRID_LOGIN, { accountId: "rdw_reserved", create: true }]
|
[IPC_CHANNELS.OPEN_REALDEBRID_LOGIN, { accountId: "rdw_reserved", create: true, dailyLimitBytes: 10_000 }]
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,14 @@ import { describe, expect, it } from "vitest";
|
|||||||
import {
|
import {
|
||||||
buildConfiguredProviderOrder,
|
buildConfiguredProviderOrder,
|
||||||
filterAccountDialogOptions,
|
filterAccountDialogOptions,
|
||||||
|
getAvailableAccountOptions,
|
||||||
getAccountDialogSelectableOptions,
|
getAccountDialogSelectableOptions,
|
||||||
isAccountRowSelectionKey,
|
isAccountRowSelectionKey,
|
||||||
matchesAccountModeFilter,
|
matchesAccountModeFilter,
|
||||||
pruneAccountRowSelection,
|
pruneAccountRowSelection,
|
||||||
resolveAccountUsername,
|
resolveAccountUsername,
|
||||||
resolveVisibleAccountKind
|
resolveVisibleAccountKind,
|
||||||
|
sortAccountServices
|
||||||
} from "../src/renderer/account-ui";
|
} from "../src/renderer/account-ui";
|
||||||
import * as accountUi from "../src/renderer/account-ui";
|
import * as accountUi from "../src/renderer/account-ui";
|
||||||
|
|
||||||
@@ -32,7 +34,40 @@ describe("account dialog filter", () => {
|
|||||||
|
|
||||||
it("combines an exact service choice with an access-type search", () => {
|
it("combines an exact service choice with an access-type search", () => {
|
||||||
expect(filterAccountDialogOptions(options, "web", "Real-Debrid").map((option) => option.id)).toEqual(["rd-web"]);
|
expect(filterAccountDialogOptions(options, "web", "Real-Debrid").map((option) => option.id)).toEqual(["rd-web"]);
|
||||||
expect(filterAccountDialogOptions(options, "api", "all").map((option) => option.id)).toEqual(["rd-api", "md-api"]);
|
expect(filterAccountDialogOptions(options, "api", "all").map((option) => option.id)).toEqual(["md-api", "rd-api"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sorts services alphabetically in German without duplicates", () => {
|
||||||
|
expect(sortAccountServices([
|
||||||
|
"Real-Debrid",
|
||||||
|
"Mega-Debrid",
|
||||||
|
"BestDebrid",
|
||||||
|
"Debrid-Link",
|
||||||
|
"1Fichier",
|
||||||
|
"AllDebrid",
|
||||||
|
"DDownload",
|
||||||
|
"LinkSnappy",
|
||||||
|
"Mega-Debrid"
|
||||||
|
])).toEqual([
|
||||||
|
"1Fichier",
|
||||||
|
"AllDebrid",
|
||||||
|
"BestDebrid",
|
||||||
|
"DDownload",
|
||||||
|
"Debrid-Link",
|
||||||
|
"LinkSnappy",
|
||||||
|
"Mega-Debrid",
|
||||||
|
"Real-Debrid"
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps both Real-Debrid access types addable after existing Real-Debrid accounts", () => {
|
||||||
|
const choices = [
|
||||||
|
{ kind: "realdebrid-api", service: "realdebrid" },
|
||||||
|
{ kind: "realdebrid-web", service: "realdebrid" },
|
||||||
|
{ kind: "bestdebrid-api", service: "bestdebrid" }
|
||||||
|
];
|
||||||
|
expect(getAvailableAccountOptions(choices, ["realdebrid", "bestdebrid"]).map((option) => option.kind))
|
||||||
|
.toEqual(["realdebrid-api", "realdebrid-web"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -174,6 +174,7 @@ describe("renderer localization", () => {
|
|||||||
["Schätzwert", "Estimate"],
|
["Schätzwert", "Estimate"],
|
||||||
["ist verfügbar. Installierte Version:", "is available. Installed version:"],
|
["ist verfügbar. Installierte Version:", "is available. Installed version:"],
|
||||||
["Tageslimit erreicht. Neue Links wechseln auf den nächsten Hoster.", "Daily limit reached. New links will switch to the next hoster."],
|
["Tageslimit erreicht. Neue Links wechseln auf den nächsten Hoster.", "Daily limit reached. New links will switch to the next hoster."],
|
||||||
|
["Der ausgewählte Real-Debrid-Account wurde nicht gefunden.", "The selected Real-Debrid account was not found."],
|
||||||
["Nur lokal", "Local only"],
|
["Nur lokal", "Local only"],
|
||||||
["Name kopiert", "Name copied"],
|
["Name kopiert", "Name copied"],
|
||||||
["Link kopiert", "Link copied"],
|
["Link kopiert", "Link copied"],
|
||||||
|
|||||||
@@ -15,11 +15,14 @@ function eventFor(url: string) {
|
|||||||
|
|
||||||
describe("ipc-security", () => {
|
describe("ipc-security", () => {
|
||||||
it("accepts only opaque Real-Debrid browser account login requests", () => {
|
it("accepts only opaque Real-Debrid browser account login requests", () => {
|
||||||
expect(validateRealDebridLoginRequest({ accountId: "rdw_existing" })).toEqual({ accountId: "rdw_existing", create: false });
|
expect(validateRealDebridLoginRequest({ accountId: "rdw_existing" })).toEqual({ accountId: "rdw_existing", create: false, dailyLimitBytes: 0 });
|
||||||
expect(validateRealDebridLoginRequest({ accountId: "rdw_reserved", create: true })).toEqual({ accountId: "rdw_reserved", create: true });
|
expect(validateRealDebridLoginRequest({ accountId: "rdw_reserved", create: true, dailyLimitBytes: 12_345.9 })).toEqual({ accountId: "rdw_reserved", create: true, dailyLimitBytes: 12_345 });
|
||||||
expect(() => validateRealDebridLoginRequest({ accountId: "../shared", create: true })).toThrow(/Account-Payload/i);
|
expect(() => validateRealDebridLoginRequest({ accountId: "../shared", create: true })).toThrow(/Account-Payload/i);
|
||||||
expect(() => validateRealDebridLoginRequest({ accountId: "rdw_valid", create: "yes" })).toThrow(/Account-Payload/i);
|
expect(() => validateRealDebridLoginRequest({ accountId: "rdw_valid", create: "yes" })).toThrow(/Account-Payload/i);
|
||||||
expect(() => validateRealDebridLoginRequest({ accountId: "rdw_valid", create: false, token: "secret" })).toThrow(/Account-Payload/i);
|
expect(() => validateRealDebridLoginRequest({ accountId: "rdw_valid", create: false, token: "secret" })).toThrow(/Account-Payload/i);
|
||||||
|
expect(() => validateRealDebridLoginRequest({ accountId: "rdw_valid", create: true, dailyLimitBytes: -1 })).toThrow(/Account-Payload/i);
|
||||||
|
expect(() => validateRealDebridLoginRequest({ accountId: "rdw_valid", create: true, dailyLimitBytes: Number.MAX_SAFE_INTEGER + 1 })).toThrow(/Account-Payload/i);
|
||||||
|
expect(() => validateRealDebridLoginRequest({ accountId: "rdw_existing", dailyLimitBytes: 1 })).toThrow(/Account-Payload/i);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("accepts IPC from the configured Vite development renderer origin", () => {
|
it("accepts IPC from the configured Vite development renderer origin", () => {
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ describe("realdebrid-web", () => {
|
|||||||
dispose: vi.fn()
|
dispose: vi.fn()
|
||||||
};
|
};
|
||||||
controller.settings = defaultSettings();
|
controller.settings = defaultSettings();
|
||||||
controller.pendingRealDebridWebAccountIds = new Map([["rdw_reserved", 0]]);
|
controller.pendingRealDebridWebAccountIds = new Map([["rdw_reserved", { generation: 0, dailyLimitBytes: 987_654 }]]);
|
||||||
controller.realDebridWebGenerations = new Map([["rdw_reserved", 0]]);
|
controller.realDebridWebGenerations = new Map([["rdw_reserved", 0]]);
|
||||||
controller.realDebridWebFallbacks = new Map([["rdw_reserved", fallback]]);
|
controller.realDebridWebFallbacks = new Map([["rdw_reserved", fallback]]);
|
||||||
controller.manager = { applyDebridAccountStatuses: applyStatuses };
|
controller.manager = { applyDebridAccountStatuses: applyStatuses };
|
||||||
@@ -272,6 +272,7 @@ describe("realdebrid-web", () => {
|
|||||||
|
|
||||||
expect(updateSettings).toHaveBeenCalledTimes(1);
|
expect(updateSettings).toHaveBeenCalledTimes(1);
|
||||||
expect(controller.settings.realDebridWebAccountIds).toEqual(["rdw_reserved"]);
|
expect(controller.settings.realDebridWebAccountIds).toEqual(["rdw_reserved"]);
|
||||||
|
expect(controller.settings.realDebridAccountDailyLimitBytes).toEqual({ rdw_reserved: 987_654 });
|
||||||
expect(applyStatuses).toHaveBeenCalledWith([
|
expect(applyStatuses).toHaveBeenCalledWith([
|
||||||
expect.objectContaining({ accountId: "rdw_reserved", valid: true, username: "fixture-user" })
|
expect.objectContaining({ accountId: "rdw_reserved", valid: true, username: "fixture-user" })
|
||||||
]);
|
]);
|
||||||
@@ -334,7 +335,7 @@ describe("realdebrid-web", () => {
|
|||||||
controller.realDebridWebAuthenticationTasks = new Map();
|
controller.realDebridWebAuthenticationTasks = new Map();
|
||||||
controller.audit = vi.fn();
|
controller.audit = vi.fn();
|
||||||
|
|
||||||
await expect(controller.openRealDebridLoginWindow({ accountId: "rdw_failed", create: true })).rejects.toThrow("load failed");
|
await expect(controller.openRealDebridLoginWindow({ accountId: "rdw_failed", create: true, dailyLimitBytes: 123_456 })).rejects.toThrow("load failed");
|
||||||
|
|
||||||
expect(controller.pendingRealDebridWebAccountIds.size).toBe(0);
|
expect(controller.pendingRealDebridWebAccountIds.size).toBe(0);
|
||||||
expect(controller.realDebridWebFallbacks.size).toBe(0);
|
expect(controller.realDebridWebFallbacks.size).toBe(0);
|
||||||
@@ -353,11 +354,12 @@ describe("realdebrid-web", () => {
|
|||||||
controller.realDebridWebAuthenticationTasks = new Map();
|
controller.realDebridWebAuthenticationTasks = new Map();
|
||||||
controller.audit = vi.fn();
|
controller.audit = vi.fn();
|
||||||
|
|
||||||
await controller.openRealDebridLoginWindow({ accountId: "rdw_closed", create: true });
|
await controller.openRealDebridLoginWindow({ accountId: "rdw_closed", create: true, dailyLimitBytes: 123_456 });
|
||||||
mockBrowserWindow.close();
|
mockBrowserWindow.close();
|
||||||
await vi.waitFor(() => expect(controller.realDebridWebFallbacks.size).toBe(0));
|
await vi.waitFor(() => expect(controller.realDebridWebFallbacks.size).toBe(0));
|
||||||
|
|
||||||
expect(controller.pendingRealDebridWebAccountIds.size).toBe(0);
|
expect(controller.pendingRealDebridWebAccountIds.size).toBe(0);
|
||||||
|
expect(controller.settings.realDebridAccountDailyLimitBytes).toEqual({});
|
||||||
expect(mockFromPartition).toHaveBeenCalledWith("persist:realdebrid-web-rdw_closed");
|
expect(mockFromPartition).toHaveBeenCalledWith("persist:realdebrid-web-rdw_closed");
|
||||||
expect(mockFromPartition).toHaveBeenCalledWith("realdebrid-web-rdw_closed");
|
expect(mockFromPartition).toHaveBeenCalledWith("realdebrid-web-rdw_closed");
|
||||||
});
|
});
|
||||||
@@ -404,13 +406,14 @@ describe("realdebrid-web", () => {
|
|||||||
});
|
});
|
||||||
controller.audit = vi.fn();
|
controller.audit = vi.fn();
|
||||||
|
|
||||||
await controller.openRealDebridLoginWindow({ accountId: "rdw_close_auth", create: true });
|
await controller.openRealDebridLoginWindow({ accountId: "rdw_close_auth", create: true, dailyLimitBytes: 123_456 });
|
||||||
await vi.waitFor(() => expect(mockExecuteJavaScript).toHaveBeenCalledTimes(1));
|
await vi.waitFor(() => expect(mockExecuteJavaScript).toHaveBeenCalledTimes(1));
|
||||||
mockBrowserWindow.close();
|
mockBrowserWindow.close();
|
||||||
resolveClosingToken("close-time-token");
|
resolveClosingToken("close-time-token");
|
||||||
await vi.waitFor(() => expect(controller.settings.realDebridWebAccountIds).toEqual(["rdw_close_auth"]));
|
await vi.waitFor(() => expect(controller.settings.realDebridWebAccountIds).toEqual(["rdw_close_auth"]));
|
||||||
|
|
||||||
expect(controller.pendingRealDebridWebAccountIds.size).toBe(0);
|
expect(controller.pendingRealDebridWebAccountIds.size).toBe(0);
|
||||||
|
expect(controller.settings.realDebridAccountDailyLimitBytes).toEqual({ rdw_close_auth: 123_456 });
|
||||||
expect(mockFromPartition).not.toHaveBeenCalledWith("persist:realdebrid-web-rdw_close_auth");
|
expect(mockFromPartition).not.toHaveBeenCalledWith("persist:realdebrid-web-rdw_close_auth");
|
||||||
expect(controller.manager.applyDebridAccountStatuses).toHaveBeenCalledWith([
|
expect(controller.manager.applyDebridAccountStatuses).toHaveBeenCalledWith([
|
||||||
expect.objectContaining({ accountId: "rdw_close_auth", valid: true, username: "close-user" })
|
expect.objectContaining({ accountId: "rdw_close_auth", valid: true, username: "close-user" })
|
||||||
|
|||||||
@@ -159,4 +159,25 @@ describe("renderer state serialization", () => {
|
|||||||
expect(serialized).not.toContain(secret);
|
expect(serialized).not.toContain(secret);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("exposes only non-secret Real-Debrid account controls to the renderer", () => {
|
||||||
|
const settings = {
|
||||||
|
...defaultSettings(),
|
||||||
|
realDebridApiTokens: JSON.stringify({ version: 1, accounts: [{ id: "rda_visible", token: "fixture-rd-secret" }] }),
|
||||||
|
realDebridDisabledAccountIds: ["rda_visible"],
|
||||||
|
realDebridAccountDailyLimitBytes: { rda_visible: 10_000 },
|
||||||
|
realDebridAccountDailyUsageBytes: { rda_visible: 2_000 },
|
||||||
|
realDebridAccountTotalUsageBytes: { rda_visible: 8_000 }
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderer = createRendererState(settings).settings;
|
||||||
|
|
||||||
|
expect(renderer.realDebridDisabledAccountIds).toEqual(["rda_visible"]);
|
||||||
|
expect(renderer.realDebridAccountDailyLimitBytes).toEqual({ rda_visible: 10_000 });
|
||||||
|
expect(renderer.realDebridAccountDailyUsageBytes).toEqual({ rda_visible: 2_000 });
|
||||||
|
expect(renderer.realDebridAccountTotalUsageBytes).toEqual({ rda_visible: 8_000 });
|
||||||
|
expect(JSON.stringify(renderer)).not.toContain("fixture-rd-secret");
|
||||||
|
expect(renderer).not.toHaveProperty("realDebridApiTokens");
|
||||||
|
expect(renderer).not.toHaveProperty("realDebridWebAccountIds");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user