fix(accounts): refresh active logins and toggle instantly
Split bulk account checks into active-only and all-configured scopes so disabled providers, Mega-Debrid accounts, and Debrid-Link keys no longer distort the normal result count. Preserve explicit full checks for diagnosing disabled credentials and surface their failed status. Render account enablement from the optimistic settings draft, persist it with revision-safe rollback, and apply the same immediate behavior to individual and bulk switches. Extend the IPC contract, translations, changelog, and regression coverage.
This commit is contained in:
@@ -2,6 +2,14 @@
|
|||||||
|
|
||||||
All notable changes to Multi-Debrid Downloader are documented in this file.
|
All notable changes to Multi-Debrid Downloader are documented in this file.
|
||||||
|
|
||||||
|
## [Unreleased]
|
||||||
|
|
||||||
|
### Accounts
|
||||||
|
|
||||||
|
- Split account refresh into active-account and all-account checks with matching result counts.
|
||||||
|
- Show failed check results for disabled accounts when all configured accounts are refreshed.
|
||||||
|
- Apply individual and bulk account enablement changes immediately while settings are saved, with automatic rollback after a failed save.
|
||||||
|
|
||||||
## [2.0.40] - 2026-08-15
|
## [2.0.40] - 2026-08-15
|
||||||
|
|
||||||
### Interface
|
### Interface
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { AppSettings, DebridAccountStatus } from "../shared/types";
|
import type { AccountCheckScope, AppSettings, DebridAccountStatus, DebridProvider } from "../shared/types";
|
||||||
import { 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 { logger } from "./logger";
|
import { logger } from "./logger";
|
||||||
import { compactErrorText } from "./utils";
|
import { compactErrorText } from "./utils";
|
||||||
@@ -270,14 +270,35 @@ export async function checkDebridLinkKey(
|
|||||||
export async function checkAllDebridAccounts(
|
export async function checkAllDebridAccounts(
|
||||||
settings: AppSettings,
|
settings: AppSettings,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
probeRealDebridWebSession?: RealDebridSessionProbe
|
probeRealDebridWebSession?: RealDebridSessionProbe,
|
||||||
|
scope: AccountCheckScope = "all"
|
||||||
): Promise<DebridAccountStatus[]> {
|
): Promise<DebridAccountStatus[]> {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const megaAccounts = parseMegaDebridAccounts(settings.megaCredentials || "", settings.megaPassword || "");
|
const providerEnabled = (provider: DebridProvider): boolean => !(settings.disabledProviders || []).includes(provider);
|
||||||
const debridLinkKeys = parseDebridLinkApiKeys(settings.debridLinkApiKeys || "");
|
const configuredMegaAccounts = parseMegaDebridAccounts(settings.megaCredentials || "", settings.megaPassword || "");
|
||||||
|
const activeMegaAccounts = (["api", "web"] as const).flatMap((mode) => {
|
||||||
|
const provider: DebridProvider = mode === "api" ? "megadebrid-api" : "megadebrid-web";
|
||||||
|
const modeEnabled = mode === "api" ? settings.megaDebridApiEnabled : settings.megaDebridWebEnabled;
|
||||||
|
if (!modeEnabled || !providerEnabled(provider) || !providerEnabled("megadebrid")) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const disabledIds = new Set(getMegaDebridDisabledAccountIdsForMode(settings, mode));
|
||||||
|
return getMegaDebridAccountsForMode(settings, mode).filter((account) => !disabledIds.has(account.id));
|
||||||
|
});
|
||||||
|
const megaAccounts = scope === "all"
|
||||||
|
? configuredMegaAccounts
|
||||||
|
: [...new Map(activeMegaAccounts.map((account) => [account.id, account])).values()];
|
||||||
|
const configuredDebridLinkKeys = parseDebridLinkApiKeys(settings.debridLinkApiKeys || "");
|
||||||
|
const debridLinkKeys = scope === "all"
|
||||||
|
? configuredDebridLinkKeys
|
||||||
|
: providerEnabled("debridlink")
|
||||||
|
? configuredDebridLinkKeys.filter((key) => !(settings.debridLinkDisabledKeyIds || []).includes(key.id))
|
||||||
|
: [];
|
||||||
|
const checkRealDebrid = Boolean(settings.realDebridUseWebLogin || String(settings.token || "").trim())
|
||||||
|
&& (scope === "all" || providerEnabled("realdebrid"));
|
||||||
|
|
||||||
const taskFns: Array<() => Promise<DebridAccountStatus>> = [
|
const taskFns: Array<() => Promise<DebridAccountStatus>> = [
|
||||||
...(settings.realDebridUseWebLogin || String(settings.token || "").trim()
|
...(checkRealDebrid
|
||||||
? [() => checkRealDebridAccount(settings, signal, now, probeRealDebridWebSession)]
|
? [() => checkRealDebridAccount(settings, signal, now, probeRealDebridWebSession)]
|
||||||
: []),
|
: []),
|
||||||
...megaAccounts.map((account) => () => checkMegaDebridAccount(account, signal, now)),
|
...megaAccounts.map((account) => () => checkMegaDebridAccount(account, signal, now)),
|
||||||
@@ -286,7 +307,7 @@ export async function checkAllDebridAccounts(
|
|||||||
|
|
||||||
const results = await runWithConcurrency(taskFns, CHECK_CONCURRENCY);
|
const results = await runWithConcurrency(taskFns, CHECK_CONCURRENCY);
|
||||||
logger.info(
|
logger.info(
|
||||||
`Account-Check abgeschlossen: ${results.length} Accounts geprueft ` +
|
`Account-Check abgeschlossen: scope=${scope}, ${results.length} Accounts geprueft ` +
|
||||||
`(${results.filter((r) => r.valid).length} gueltig, ${results.filter((r) => r.isPremium).length} premium)`
|
`(${results.filter((r) => r.valid).length} gueltig, ${results.filter((r) => r.isPremium).length} premium)`
|
||||||
);
|
);
|
||||||
return results;
|
return results;
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import os from "node:os";
|
|||||||
import v8 from "node:v8";
|
import v8 from "node:v8";
|
||||||
import { app } from "electron";
|
import { app } from "electron";
|
||||||
import {
|
import {
|
||||||
AddLinksPayload,
|
AddLinksPayload,
|
||||||
AllDebridHostInfo,
|
AccountCheckScope,
|
||||||
|
AllDebridHostInfo,
|
||||||
AppSettings,
|
AppSettings,
|
||||||
AccountCommand,
|
AccountCommand,
|
||||||
AccountCommandResult,
|
AccountCommandResult,
|
||||||
@@ -647,18 +648,20 @@ export class AppController {
|
|||||||
return fetchDebridLinkHostLimits(this.settings.debridLinkApiKeys, host);
|
return fetchDebridLinkHostLimits(this.settings.debridLinkApiKeys, host);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
|
public async checkDebridAccounts(scope: AccountCheckScope = "active"): Promise<DebridAccountStatus[]> {
|
||||||
const statuses = sanitizeDebridAccountStatuses(
|
const statuses = sanitizeDebridAccountStatuses(
|
||||||
await checkAllDebridAccounts(
|
await checkAllDebridAccounts(
|
||||||
this.settings,
|
this.settings,
|
||||||
undefined,
|
undefined,
|
||||||
(signal) => this.realDebridWebFallback.probeLoginState(signal)
|
(signal) => this.realDebridWebFallback.probeLoginState(signal),
|
||||||
|
scope
|
||||||
),
|
),
|
||||||
collectAccountStatusRedactionValues(this.settings)
|
collectAccountStatusRedactionValues(this.settings)
|
||||||
);
|
);
|
||||||
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,
|
||||||
|
scope,
|
||||||
valid: statuses.filter((s) => s.valid).length,
|
valid: statuses.filter((s) => s.valid).length,
|
||||||
premium: statuses.filter((s) => s.isPremium).length
|
premium: statuses.filter((s) => s.isPremium).length
|
||||||
});
|
});
|
||||||
|
|||||||
+6
-2
@@ -843,8 +843,12 @@ function registerIpcHandlers(): void {
|
|||||||
return controller.getDebridLinkHostLimits();
|
return controller.getDebridLinkHostLimits();
|
||||||
});
|
});
|
||||||
|
|
||||||
handleTrusted(IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS, async () => {
|
handleTrusted(IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS, async (_event, rawScope: unknown) => {
|
||||||
return controller.checkDebridAccounts();
|
const scope = rawScope === undefined ? "active" : validateString(rawScope, "scope");
|
||||||
|
if (scope !== "active" && scope !== "all") {
|
||||||
|
throw new Error("scope ist ungültig");
|
||||||
|
}
|
||||||
|
return controller.checkDebridAccounts(scope);
|
||||||
});
|
});
|
||||||
|
|
||||||
handleTrusted(IPC_CHANNELS.CHECK_ACCOUNT_CREDENTIALS, async (_event, rawInput: unknown) => {
|
handleTrusted(IPC_CHANNELS.CHECK_ACCOUNT_CREDENTIALS, async (_event, rawInput: unknown) => {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { contextBridge, ipcRenderer } from "electron";
|
import { contextBridge, ipcRenderer } from "electron";
|
||||||
import {
|
import {
|
||||||
AddLinksPayload,
|
AddLinksPayload,
|
||||||
|
AccountCheckScope,
|
||||||
AccountCommandResult,
|
AccountCommandResult,
|
||||||
AccountCredentialCheckInput,
|
AccountCredentialCheckInput,
|
||||||
AccountCreateCommand,
|
AccountCreateCommand,
|
||||||
@@ -103,7 +104,7 @@ const api: ElectronApi = {
|
|||||||
importBestDebridCookies: (): Promise<number> => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_BESTDEBRID_COOKIES),
|
importBestDebridCookies: (): Promise<number> => ipcRenderer.invoke(IPC_CHANNELS.IMPORT_BESTDEBRID_COOKIES),
|
||||||
getAllDebridHostInfo: (): Promise<AllDebridHostInfo> => ipcRenderer.invoke(IPC_CHANNELS.GET_ALLDEBRID_HOST_INFO),
|
getAllDebridHostInfo: (): Promise<AllDebridHostInfo> => ipcRenderer.invoke(IPC_CHANNELS.GET_ALLDEBRID_HOST_INFO),
|
||||||
getDebridLinkHostLimits: (): Promise<DebridLinkHostLimitInfo[]> => ipcRenderer.invoke(IPC_CHANNELS.GET_DEBRIDLINK_HOST_LIMITS),
|
getDebridLinkHostLimits: (): Promise<DebridLinkHostLimitInfo[]> => ipcRenderer.invoke(IPC_CHANNELS.GET_DEBRIDLINK_HOST_LIMITS),
|
||||||
checkDebridAccounts: (): Promise<DebridAccountStatus[]> => ipcRenderer.invoke(IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS),
|
checkDebridAccounts: (scope: AccountCheckScope = "active"): Promise<DebridAccountStatus[]> => ipcRenderer.invoke(IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS, scope),
|
||||||
checkAccountCredentials: (input: AccountCredentialCheckInput): Promise<DebridAccountStatus> => ipcRenderer.invoke(IPC_CHANNELS.CHECK_ACCOUNT_CREDENTIALS, input),
|
checkAccountCredentials: (input: AccountCredentialCheckInput): Promise<DebridAccountStatus> => ipcRenderer.invoke(IPC_CHANNELS.CHECK_ACCOUNT_CREDENTIALS, input),
|
||||||
retryExtraction: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.RETRY_EXTRACTION, packageId),
|
retryExtraction: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.RETRY_EXTRACTION, packageId),
|
||||||
extractNow: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.EXTRACT_NOW, packageId),
|
extractNow: (packageId: string): Promise<void> => ipcRenderer.invoke(IPC_CHANNELS.EXTRACT_NOW, packageId),
|
||||||
|
|||||||
+71
-50
@@ -40,7 +40,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, shouldClearDownloadSelection, shouldClearDownloadSelectionOnEscape } from "./selection";
|
import { pruneSelection, shouldClearDownloadSelection, shouldClearDownloadSelectionOnEscape } from "./selection";
|
||||||
import { buildBulkAccountEnabledState, buildConfiguredProviderOrder, getAccountDialogSelectableOptions, matchesAccountModeFilter, pruneAccountRowSelection, resolveAccountUsername, resolveVisibleAccountKind } from "./account-ui";
|
import { buildBulkAccountEnabledState, buildConfiguredProviderOrder, getAccountDialogSelectableOptions, matchesAccountModeFilter, pruneAccountRowSelection, resolveAccountStatusState, resolveAccountUsername, resolveVisibleAccountKind, runOptimisticAccountUpdate } from "./account-ui";
|
||||||
import type { AccountModeFilter } from "./account-ui";
|
import type { AccountModeFilter } from "./account-ui";
|
||||||
import { buildAccountDeleteCommand, buildAccountReplaceCommand, createAccountEditState, validateAccountEdit } from "./account-edit";
|
import { buildAccountDeleteCommand, buildAccountReplaceCommand, 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";
|
||||||
@@ -2245,7 +2245,7 @@ export function App(): ReactElement {
|
|||||||
label: `Key ${index + 1}`,
|
label: `Key ${index + 1}`,
|
||||||
token: "",
|
token: "",
|
||||||
masked: account.maskedIdentity,
|
masked: account.maskedIdentity,
|
||||||
disabled: !account.enabled,
|
disabled: settingsDraft.debridLinkDisabledKeyIds.includes(account.accountId),
|
||||||
dailyUsedBytes: keyDailyUsedBytes,
|
dailyUsedBytes: keyDailyUsedBytes,
|
||||||
totalUsedBytes: account.totalUsageBytes,
|
totalUsedBytes: account.totalUsageBytes,
|
||||||
dailyLimitBytes: keyDailyLimitBytes,
|
dailyLimitBytes: keyDailyLimitBytes,
|
||||||
@@ -2318,7 +2318,9 @@ export function App(): ReactElement {
|
|||||||
credentialLabel: "••••••",
|
credentialLabel: "••••••",
|
||||||
accountId: acc.accountId,
|
accountId: acc.accountId,
|
||||||
checkable: true,
|
checkable: true,
|
||||||
disabled: !acc.enabled,
|
disabled: entry.disabled || (entry.kind === "megadebrid-api"
|
||||||
|
? settingsDraft.megaDebridApiDisabledAccountIds.includes(acc.accountId)
|
||||||
|
: settingsDraft.megaDebridWebDisabledAccountIds.includes(acc.accountId)),
|
||||||
dailyUsedBytes: used,
|
dailyUsedBytes: used,
|
||||||
dailyLimitBytes: limit,
|
dailyLimitBytes: limit,
|
||||||
dailyRemainingBytes: limit > 0 ? Math.max(0, limit - used) : 0,
|
dailyRemainingBytes: limit > 0 ? Math.max(0, limit - used) : 0,
|
||||||
@@ -2344,7 +2346,7 @@ export function App(): ReactElement {
|
|||||||
credentialLabel: "API-Key",
|
credentialLabel: "API-Key",
|
||||||
accountId: key.id,
|
accountId: key.id,
|
||||||
checkable: true,
|
checkable: true,
|
||||||
disabled: key.disabled,
|
disabled: entry.disabled || settingsDraft.debridLinkDisabledKeyIds.includes(key.id),
|
||||||
dailyUsedBytes: key.dailyUsedBytes,
|
dailyUsedBytes: key.dailyUsedBytes,
|
||||||
dailyLimitBytes: key.dailyLimitBytes,
|
dailyLimitBytes: key.dailyLimitBytes,
|
||||||
dailyRemainingBytes: key.dailyLimitBytes > 0 ? Math.max(0, key.dailyLimitBytes - key.dailyUsedBytes) : 0,
|
dailyRemainingBytes: key.dailyLimitBytes > 0 ? Math.max(0, key.dailyLimitBytes - key.dailyUsedBytes) : 0,
|
||||||
@@ -2388,7 +2390,7 @@ export function App(): ReactElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
return rows;
|
return rows;
|
||||||
}, [configuredAccounts, snapshot.accounts]);
|
}, [configuredAccounts, settingsDraft, snapshot.accounts]);
|
||||||
|
|
||||||
const [accountStatusSort, setAccountStatusSort] = useState<"none" | "desc" | "asc">("none");
|
const [accountStatusSort, setAccountStatusSort] = useState<"none" | "desc" | "asc">("none");
|
||||||
const cycleAccountStatusSort = (): void => setAccountStatusSort((s) => (s === "none" ? "desc" : s === "desc" ? "asc" : "none"));
|
const cycleAccountStatusSort = (): void => setAccountStatusSort((s) => (s === "none" ? "desc" : s === "desc" ? "asc" : "none"));
|
||||||
@@ -2651,16 +2653,17 @@ export function App(): ReactElement {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkAllAccounts = useCallback(async (): Promise<void> => {
|
const checkAccounts = useCallback(async (scope: "active" | "all"): Promise<void> => {
|
||||||
setAccountCheckBusy(true);
|
setAccountCheckBusy(true);
|
||||||
try {
|
try {
|
||||||
const statuses = await window.rd.checkDebridAccounts();
|
const statuses = await window.rd.checkDebridAccounts(scope);
|
||||||
if (!statuses || statuses.length === 0) {
|
if (!statuses || statuses.length === 0) {
|
||||||
showToast("Keine prüfbaren Accounts konfiguriert.", 3200);
|
showToast(scope === "active" ? "Keine aktiven prüfbaren Accounts konfiguriert." : "Keine prüfbaren Accounts konfiguriert.", 3200);
|
||||||
} else {
|
} else {
|
||||||
const valid = statuses.filter((st) => st.valid).length;
|
const valid = statuses.filter((st) => st.valid).length;
|
||||||
const premium = statuses.filter((st) => st.isPremium).length;
|
const premium = statuses.filter((st) => st.isPremium).length;
|
||||||
showToast(`Account-Check: ${valid}/${statuses.length} Login gültig, ${premium} mit Premium.`, 3600);
|
const label = scope === "active" ? "Aktive Accounts" : "Alle Accounts";
|
||||||
|
showToast(`${label}: ${valid}/${statuses.length} Login gültig, ${premium} mit Premium.`, 3600);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showToast(`Account-Check fehlgeschlagen: ${String(error)}`, 3600);
|
showToast(`Account-Check fehlgeschlagen: ${String(error)}`, 3600);
|
||||||
@@ -2767,7 +2770,7 @@ export function App(): ReactElement {
|
|||||||
} else if (selectedOption) {
|
} else if (selectedOption) {
|
||||||
showToast(`${selectedOption.title} gespeichert`, 2200);
|
showToast(`${selectedOption.title} gespeichert`, 2200);
|
||||||
}
|
}
|
||||||
void checkAllAccounts();
|
void checkAccounts("active");
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
showToast(`Account konnte nicht gespeichert werden: ${String(error)}`, 3200);
|
showToast(`Account konnte nicht gespeichert werden: ${String(error)}`, 3200);
|
||||||
});
|
});
|
||||||
@@ -2783,31 +2786,56 @@ export function App(): ReactElement {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onResetDebridLinkApiKeyDailyUsage = async (entry: ConfiguredAccountEntry, keyId: string, keyLabel: string): Promise<void> => {
|
const onResetDebridLinkApiKeyDailyUsage = async (entry: ConfiguredAccountEntry, keyId: string, keyLabel: string): Promise<void> => {
|
||||||
await performQuickAction(async () => {
|
await performQuickAction(async () => {
|
||||||
const result = await window.rd.resetDebridLinkApiKeyDailyUsage(keyId);
|
const result = await window.rd.resetDebridLinkApiKeyDailyUsage(keyId);
|
||||||
syncLiveProviderUsageSettings(result);
|
syncLiveProviderUsageSettings(result);
|
||||||
showToast(`${entry.serviceLabel} ${keyLabel}: Tageszähler zurückgesetzt`, 2200);
|
showToast(`${entry.serviceLabel} ${keyLabel}: Tageszähler zurückgesetzt`, 2200);
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
showToast(`${entry.serviceLabel} ${keyLabel}: Reset fehlgeschlagen: ${String(error)}`, 3200);
|
showToast(`${entry.serviceLabel} ${keyLabel}: Reset fehlgeschlagen: ${String(error)}`, 3200);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const onToggleDebridLinkApiKeyEnabled = async (entry: ConfiguredAccountEntry, key: DebridLinkAccountKeyEntry): Promise<void> => {
|
const persistAccountToggle = async (nextDraft: RendererSettingsDraft): Promise<RendererSettings> => {
|
||||||
await performQuickAction(async () => {
|
const previousDraft = settingsDraft;
|
||||||
const currentDisabledIds = settingsDraft.debridLinkDisabledKeyIds || [];
|
const previousDirty = settingsDirtyRef.current;
|
||||||
const nextDisabledIds = key.disabled
|
const previousSaveState = settingsSaveState;
|
||||||
? currentDisabledIds.filter((existingId) => existingId !== key.id)
|
const revision = ++settingsDraftRevisionRef.current;
|
||||||
: [...currentDisabledIds, key.id];
|
return runOptimisticAccountUpdate(
|
||||||
|
() => {
|
||||||
|
settingsDirtyRef.current = true;
|
||||||
|
setSettingsDirty(true);
|
||||||
|
setSettingsSaveState("saving");
|
||||||
|
setSettingsDraft(nextDraft);
|
||||||
|
},
|
||||||
|
() => persistSpecificSettings(nextDraft),
|
||||||
|
() => {
|
||||||
|
if (settingsDraftRevisionRef.current !== revision) return;
|
||||||
|
settingsDraftRevisionRef.current += 1;
|
||||||
|
settingsDirtyRef.current = previousDirty;
|
||||||
|
setSettingsDirty(previousDirty);
|
||||||
|
setSettingsSaveState(previousSaveState);
|
||||||
|
setSettingsDraft(previousDraft);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onToggleDebridLinkApiKeyEnabled = async (entry: ConfiguredAccountEntry, key: DebridLinkAccountKeyEntry): Promise<void> => {
|
||||||
|
await performQuickAction(async () => {
|
||||||
|
const currentDisabledIds = settingsDraft.debridLinkDisabledKeyIds || [];
|
||||||
|
const currentlyDisabled = currentDisabledIds.includes(key.id);
|
||||||
|
const nextDisabledIds = currentlyDisabled
|
||||||
|
? currentDisabledIds.filter((existingId) => existingId !== key.id)
|
||||||
|
: [...currentDisabledIds, key.id];
|
||||||
const nextDraft: RendererSettingsDraft = {
|
const nextDraft: RendererSettingsDraft = {
|
||||||
...settingsDraft,
|
...settingsDraft,
|
||||||
debridLinkDisabledKeyIds: nextDisabledIds
|
debridLinkDisabledKeyIds: nextDisabledIds
|
||||||
};
|
};
|
||||||
await persistSpecificSettings(nextDraft);
|
await persistAccountToggle(nextDraft);
|
||||||
showToast(
|
showToast(
|
||||||
key.disabled
|
currentlyDisabled
|
||||||
? `${entry.serviceLabel} ${key.label} aktiviert`
|
? `${entry.serviceLabel} ${key.label} aktiviert`
|
||||||
: `${entry.serviceLabel} ${key.label} deaktiviert`,
|
: `${entry.serviceLabel} ${key.label} deaktiviert`,
|
||||||
2200
|
2200
|
||||||
);
|
);
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
@@ -2834,7 +2862,7 @@ export function App(): ReactElement {
|
|||||||
const next = currentlyDisabled ? current.filter((id) => id !== accountId) : [...current, accountId];
|
const next = currentlyDisabled ? current.filter((id) => id !== accountId) : [...current, accountId];
|
||||||
const apiDisabledIds = mode === "api" ? next : settingsDraft.megaDebridApiDisabledAccountIds;
|
const apiDisabledIds = mode === "api" ? next : settingsDraft.megaDebridApiDisabledAccountIds;
|
||||||
const webDisabledIds = mode === "web" ? next : settingsDraft.megaDebridWebDisabledAccountIds;
|
const webDisabledIds = mode === "web" ? next : settingsDraft.megaDebridWebDisabledAccountIds;
|
||||||
await persistSpecificSettings({
|
await persistAccountToggle({
|
||||||
...settingsDraft,
|
...settingsDraft,
|
||||||
megaDebridDisabledAccountIds: [...new Set([...apiDisabledIds, ...webDisabledIds])],
|
megaDebridDisabledAccountIds: [...new Set([...apiDisabledIds, ...webDisabledIds])],
|
||||||
megaDebridApiDisabledAccountIds: apiDisabledIds,
|
megaDebridApiDisabledAccountIds: apiDisabledIds,
|
||||||
@@ -2865,10 +2893,10 @@ export function App(): ReactElement {
|
|||||||
? current.filter((existing) => existing !== provider)
|
? current.filter((existing) => existing !== provider)
|
||||||
: [...current, provider];
|
: [...current, provider];
|
||||||
const nextDraft: RendererSettingsDraft = {
|
const nextDraft: RendererSettingsDraft = {
|
||||||
...settingsDraft,
|
...settingsDraft,
|
||||||
disabledProviders: nextDisabledProviders
|
disabledProviders: nextDisabledProviders
|
||||||
};
|
};
|
||||||
await persistSpecificSettings(nextDraft);
|
await persistAccountToggle(nextDraft);
|
||||||
showToast(
|
showToast(
|
||||||
nextDisabledProviders.includes(provider)
|
nextDisabledProviders.includes(provider)
|
||||||
? `${entry.serviceLabel} deaktiviert`
|
? `${entry.serviceLabel} deaktiviert`
|
||||||
@@ -2907,7 +2935,7 @@ export function App(): ReactElement {
|
|||||||
megaDebridApiDisabledAccountIds: enabled ? [] : accountRows.filter((row) => row.entry.kind === "megadebrid-api" && row.accountId).map((row) => row.accountId as string),
|
megaDebridApiDisabledAccountIds: enabled ? [] : accountRows.filter((row) => row.entry.kind === "megadebrid-api" && row.accountId).map((row) => row.accountId as string),
|
||||||
megaDebridWebDisabledAccountIds: enabled ? [] : accountRows.filter((row) => row.entry.kind === "megadebrid-web" && row.accountId).map((row) => row.accountId as string)
|
megaDebridWebDisabledAccountIds: enabled ? [] : accountRows.filter((row) => row.entry.kind === "megadebrid-web" && row.accountId).map((row) => row.accountId as string)
|
||||||
};
|
};
|
||||||
await persistSpecificSettings(nextDraft);
|
await persistAccountToggle(nextDraft);
|
||||||
showToast(enabled ? "Accounts aktiviert" : "Accounts deaktiviert", 2200);
|
showToast(enabled ? "Accounts aktiviert" : "Accounts deaktiviert", 2200);
|
||||||
}, (error) => {
|
}, (error) => {
|
||||||
showToast(`Accounts konnten nicht umgeschaltet werden: ${String(error)}`, 3200);
|
showToast(`Accounts konnten nicht umgeschaltet werden: ${String(error)}`, 3200);
|
||||||
@@ -2944,7 +2972,7 @@ export function App(): ReactElement {
|
|||||||
const checkAccountTableRow = (row: AccountTableRow): void => {
|
const checkAccountTableRow = (row: AccountTableRow): void => {
|
||||||
setAccountContextMenu(null);
|
setAccountContextMenu(null);
|
||||||
if (row.checkable) {
|
if (row.checkable) {
|
||||||
void checkAllAccounts();
|
void checkAccounts("all");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (getAccountQuickActionMeta(row.entry.kind)) {
|
if (getAccountQuickActionMeta(row.entry.kind)) {
|
||||||
@@ -4831,15 +4859,7 @@ export function App(): ReactElement {
|
|||||||
: null;
|
: null;
|
||||||
const accountSources = useMemo<AccountRowSource[]>(() => accountRows.map((row) => {
|
const accountSources = useMemo<AccountRowSource[]>(() => accountRows.map((row) => {
|
||||||
const checkedStatus = row.accountId ? snapshot.settings.debridAccountStatuses?.[row.accountId] : undefined;
|
const checkedStatus = row.accountId ? snapshot.settings.debridAccountStatuses?.[row.accountId] : undefined;
|
||||||
const state: AccountRowSource["status"]["state"] = row.disabled
|
const state: AccountRowSource["status"]["state"] = resolveAccountStatusState(row.disabled, checkedStatus);
|
||||||
? "disabled"
|
|
||||||
: !checkedStatus
|
|
||||||
? "unchecked"
|
|
||||||
: checkedStatus && !checkedStatus.valid
|
|
||||||
? "invalid"
|
|
||||||
: checkedStatus && !checkedStatus.isPremium
|
|
||||||
? "free"
|
|
||||||
: "premium";
|
|
||||||
return {
|
return {
|
||||||
identityId: row.accountId || row.rowKey,
|
identityId: row.accountId || row.rowKey,
|
||||||
service: row.entry.service,
|
service: row.entry.service,
|
||||||
@@ -4940,7 +4960,8 @@ export function App(): ReactElement {
|
|||||||
const row = selectedAccountViewId ? accountRowBindings.get(selectedAccountViewId) : null;
|
const row = selectedAccountViewId ? accountRowBindings.get(selectedAccountViewId) : null;
|
||||||
if (row) removeAccountTableRow(row);
|
if (row) removeAccountTableRow(row);
|
||||||
},
|
},
|
||||||
onCheckAll: () => { void checkAllAccounts(); },
|
onCheckActive: () => { void checkAccounts("active"); },
|
||||||
|
onCheckAll: () => { void checkAccounts("all"); },
|
||||||
onSetAllEnabled: (enabled) => { void setAllAccountsEnabled(enabled); },
|
onSetAllEnabled: (enabled) => { void setAllAccountsEnabled(enabled); },
|
||||||
onStatusSort: cycleAccountStatusSort,
|
onStatusSort: cycleAccountStatusSort,
|
||||||
onMoveProvider: (index, direction) => {
|
onMoveProvider: (index, direction) => {
|
||||||
|
|||||||
@@ -59,6 +59,36 @@ export function resolveAccountUsername(storedUsername: string, checkedEmail?: st
|
|||||||
return checkedEmail?.trim() || storedUsername.trim() || "—";
|
return checkedEmail?.trim() || storedUsername.trim() || "—";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function resolveAccountStatusState(
|
||||||
|
disabled: boolean,
|
||||||
|
checkedStatus?: { valid: boolean; isPremium: boolean }
|
||||||
|
): "disabled" | "unchecked" | "invalid" | "free" | "premium" {
|
||||||
|
if (checkedStatus && !checkedStatus.valid) {
|
||||||
|
return "invalid";
|
||||||
|
}
|
||||||
|
if (disabled) {
|
||||||
|
return "disabled";
|
||||||
|
}
|
||||||
|
if (!checkedStatus) {
|
||||||
|
return "unchecked";
|
||||||
|
}
|
||||||
|
return checkedStatus.isPremium ? "premium" : "free";
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runOptimisticAccountUpdate<T>(
|
||||||
|
apply: () => void,
|
||||||
|
persist: () => Promise<T>,
|
||||||
|
rollback: () => void
|
||||||
|
): Promise<T> {
|
||||||
|
apply();
|
||||||
|
try {
|
||||||
|
return await persist();
|
||||||
|
} catch (error) {
|
||||||
|
rollback();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function buildBulkAccountEnabledState(
|
export function buildBulkAccountEnabledState(
|
||||||
currentDisabledProviders: DebridProvider[],
|
currentDisabledProviders: DebridProvider[],
|
||||||
configuredProviders: DebridProvider[],
|
configuredProviders: DebridProvider[],
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ const pairs = [
|
|||||||
["Integritätsprüfung und Aufräumen nach Downloads und Entpacken.", "Integrity checks and cleanup after downloading and extraction."], ["Prüfung", "Verification"], ["Dateien auf Fehler prüfen", "Check files for errors"],
|
["Integritätsprüfung und Aufräumen nach Downloads und Entpacken.", "Integrity checks and cleanup after downloading and extraction."], ["Prüfung", "Verification"], ["Dateien auf Fehler prüfen", "Check files for errors"],
|
||||||
["Nach dem Entpacken", "After extraction"], ["Link-Dateien danach entfernen", "Remove link files afterward"], ["Vorschau-Dateien danach entfernen", "Remove sample files afterward"], ["Archive nach dem Entpacken", "Archives after extraction"],
|
["Nach dem Entpacken", "After extraction"], ["Link-Dateien danach entfernen", "Remove link files afterward"], ["Vorschau-Dateien danach entfernen", "Remove sample files afterward"], ["Archive nach dem Entpacken", "Archives after extraction"],
|
||||||
["Fertige Downloads und Konflikte", "Completed downloads and conflicts"], ["Fertige Downloads aus der Liste", "Completed downloads in the list"], ["Bei gleichnamigen Dateien", "When files have the same name"],
|
["Fertige Downloads und Konflikte", "Completed downloads and conflicts"], ["Fertige Downloads aus der Liste", "Completed downloads in the list"], ["Bei gleichnamigen Dateien", "When files have the same name"],
|
||||||
["Hinzufügen", "Add"], ["Aktualisieren", "Refresh"], ["+ Hinzufügen", "+ Add"], ["− Entfernen", "− Remove"], ["↻ Aktualisieren", "↻ Refresh"], ["Provider-Reihenfolge", "Provider order"], ["Lege fest, in welcher Reihenfolge verfügbare Provider verwendet werden.", "Choose the order in which available providers are used."],
|
["Hinzufügen", "Add"], ["Aktualisieren", "Refresh"], ["+ Hinzufügen", "+ Add"], ["− Entfernen", "− Remove"], ["↻ Aktualisieren", "↻ Refresh"], ["↻ Aktive aktualisieren", "↻ Refresh active"], ["↻ Alle aktualisieren", "↻ Refresh all"], ["Prüft nur aktivierte Accounts.", "Checks enabled accounts only."], ["Prüft alle angelegten Accounts, auch deaktivierte.", "Checks all configured accounts, including disabled accounts."], ["Provider-Reihenfolge", "Provider order"], ["Lege fest, in welcher Reihenfolge verfügbare Provider verwendet werden.", "Choose the order in which available providers are used."],
|
||||||
["Automatischer Fallback", "Automatic fallback"], ["Zugangsdaten lokal speichern", "Store credentials locally"], ["Hoster-Routing", "Hoster routing"], ["Eigene Zuordnungen überschreiben für den jeweiligen Hoster die Standardreihenfolge.", "Custom assignments override the default order for each hoster."],
|
["Automatischer Fallback", "Automatic fallback"], ["Zugangsdaten lokal speichern", "Store credentials locally"], ["Hoster-Routing", "Hoster routing"], ["Eigene Zuordnungen überschreiben für den jeweiligen Hoster die Standardreihenfolge.", "Custom assignments override the default order for each hoster."],
|
||||||
["Rotations-Verlauf", "Rotation history"], ["Sammlung", "Collection"], ["URL oder Rohzeile", "URL or raw line"], ["Zeile", "Line"], ["Paket / Datei", "Package / file"], ["Größe", "Size"], ["Gestartet", "Started"], ["Beendet", "Finished"],
|
["Rotations-Verlauf", "Rotation history"], ["Sammlung", "Collection"], ["URL oder Rohzeile", "URL or raw line"], ["Zeile", "Line"], ["Paket / Datei", "Package / file"], ["Größe", "Size"], ["Gestartet", "Started"], ["Beendet", "Finished"],
|
||||||
["Alle Einträge", "All entries"], ["Heute", "Today"], ["Letzte 7 Tage", "Last 7 days"], ["Älter", "Older"], ["Gelöscht", "Deleted"], ["Fehlgeschlagen", "Failed"],
|
["Alle Einträge", "All entries"], ["Heute", "Today"], ["Letzte 7 Tage", "Last 7 days"], ["Älter", "Older"], ["Gelöscht", "Deleted"], ["Fehlgeschlagen", "Failed"],
|
||||||
@@ -122,7 +122,7 @@ const pairs = [
|
|||||||
["Rapidgator-Status kann direkt aus der Liste geladen werden.", "RapidGator status can be loaded directly from the list."], ["Status basiert auf den zuletzt gespeicherten AllDebrid-Daten.", "Status is based on the last saved AllDebrid data."],
|
["Rapidgator-Status kann direkt aus der Liste geladen werden.", "RapidGator status can be loaded directly from the list."], ["Status basiert auf den zuletzt gespeicherten AllDebrid-Daten.", "Status is based on the last saved AllDebrid data."],
|
||||||
["Update wird vorbereitet", "Preparing update"], ["Stilles Update gestartet - App wird neu gestartet", "Silent update started - the app will restart"], ["Einstellungen gespeichert", "Settings saved"],
|
["Update wird vorbereitet", "Preparing update"], ["Stilles Update gestartet - App wird neu gestartet", "Silent update started - the app will restart"], ["Einstellungen gespeichert", "Settings saved"],
|
||||||
["Real-Debrid Login-Fenster geöffnet", "Real-Debrid login window opened"], ["AllDebrid Login-Fenster geöffnet", "AllDebrid login window opened"], ["Keine Cookie-Datei ausgewählt", "No cookie file selected"],
|
["Real-Debrid Login-Fenster geöffnet", "Real-Debrid login window opened"], ["AllDebrid Login-Fenster geöffnet", "AllDebrid login window opened"], ["Keine Cookie-Datei ausgewählt", "No cookie file selected"],
|
||||||
["Keine Mega-Debrid-/Debrid-Link-Accounts zum Prüfen konfiguriert.", "No Mega-Debrid or Debrid-Link accounts configured for checking."], ["Account aktiviert", "Account enabled"], ["Account deaktiviert", "Account disabled"],
|
["Keine Mega-Debrid-/Debrid-Link-Accounts zum Prüfen konfiguriert.", "No Mega-Debrid or Debrid-Link accounts configured for checking."], ["Keine prüfbaren Accounts konfiguriert.", "No checkable accounts configured."], ["Keine aktiven prüfbaren Accounts konfiguriert.", "No active checkable accounts configured."], ["Account aktiviert", "Account enabled"], ["Account deaktiviert", "Account disabled"],
|
||||||
["Account entfernen", "Remove account"], ["Account entfernt", "Account removed"], ["Key entfernen", "Remove key"], ["Key entfernt", "Key removed"], ["Accounts aktiviert", "Accounts enabled"], ["Accounts deaktiviert", "Accounts disabled"],
|
["Account entfernen", "Remove account"], ["Account entfernt", "Account removed"], ["Key entfernen", "Remove key"], ["Key entfernt", "Key removed"], ["Accounts aktiviert", "Accounts enabled"], ["Accounts deaktiviert", "Accounts disabled"],
|
||||||
["Für diesen Account ist keine direkte Statusprüfung verfügbar.", "Direct status checking is not available for this account."], ["Keine gespeicherten Links vorhanden", "No saved links available"], ["Keine Links hinzugefügt", "No links added"],
|
["Für diesen Account ist keine direkte Statusprüfung verfügbar.", "Direct status checking is not available for this account."], ["Keine gespeicherten Links vorhanden", "No saved links available"], ["Keine Links hinzugefügt", "No links added"],
|
||||||
["Fehler beim Hinzufügen", "Error while adding"], ["Verlaufseintrag entfernen", "Remove history entry"], ["Verlaufseinträge entfernen", "Remove history entries"], ["Diesen Eintrag aus dem Verlauf entfernen?", "Remove this entry from history?"],
|
["Fehler beim Hinzufügen", "Error while adding"], ["Verlaufseintrag entfernen", "Remove history entry"], ["Verlaufseinträge entfernen", "Remove history entries"], ["Diesen Eintrag aus dem Verlauf entfernen?", "Remove this entry from history?"],
|
||||||
@@ -288,6 +288,8 @@ function translateDynamic(value: string, language: AppLanguage): string {
|
|||||||
if (disabledKeys) return `${disabledKeys[1]} API keys disabled.`;
|
if (disabledKeys) return `${disabledKeys[1]} API keys disabled.`;
|
||||||
const accountCheck = value.match(/^Account-Check: (\d+\/\d+) Login gültig, (\d+) mit Premium\.$/);
|
const accountCheck = value.match(/^Account-Check: (\d+\/\d+) Login gültig, (\d+) mit Premium\.$/);
|
||||||
if (accountCheck) return `Account check: ${accountCheck[1]} logins valid, ${accountCheck[2]} with premium.`;
|
if (accountCheck) return `Account check: ${accountCheck[1]} logins valid, ${accountCheck[2]} with premium.`;
|
||||||
|
const scopedAccountCheck = value.match(/^(Aktive|Alle) Accounts: (\d+\/\d+) Login gültig, (\d+) mit Premium\.$/);
|
||||||
|
if (scopedAccountCheck) return `${scopedAccountCheck[1] === "Aktive" ? "Active" : "All"} accounts: ${scopedAccountCheck[2]} logins valid, ${scopedAccountCheck[3]} with premium.`;
|
||||||
const removeAccount = value.match(/^Soll (.+) wirklich aus der Accountliste entfernt werden\?$/);
|
const removeAccount = value.match(/^Soll (.+) wirklich aus der Accountliste entfernt werden\?$/);
|
||||||
if (removeAccount) return `Remove ${removeAccount[1]} from the account list?`;
|
if (removeAccount) return `Remove ${removeAccount[1]} from the account list?`;
|
||||||
const resolved = value.match(/^Konflikte gelöst: (\d+) überschrieben, (\d+) übersprungen$/);
|
const resolved = value.match(/^Konflikte gelöst: (\d+) überschrieben, (\d+) übersprungen$/);
|
||||||
@@ -441,6 +443,8 @@ function translateDynamic(value: string, language: AppLanguage): string {
|
|||||||
if (disabledKeys) return `${disabledKeys[1]} API-Keys deaktiviert.`;
|
if (disabledKeys) return `${disabledKeys[1]} API-Keys deaktiviert.`;
|
||||||
const accountCheck = value.match(/^Account check: (\d+\/\d+) logins valid, (\d+) with premium\.$/);
|
const accountCheck = value.match(/^Account check: (\d+\/\d+) logins valid, (\d+) with premium\.$/);
|
||||||
if (accountCheck) return `Account-Check: ${accountCheck[1]} Login gültig, ${accountCheck[2]} mit Premium.`;
|
if (accountCheck) return `Account-Check: ${accountCheck[1]} Login gültig, ${accountCheck[2]} mit Premium.`;
|
||||||
|
const scopedAccountCheck = value.match(/^(Active|All) accounts: (\d+\/\d+) logins valid, (\d+) with premium\.$/);
|
||||||
|
if (scopedAccountCheck) return `${scopedAccountCheck[1] === "Active" ? "Aktive" : "Alle"} Accounts: ${scopedAccountCheck[2]} Login gültig, ${scopedAccountCheck[3]} mit Premium.`;
|
||||||
const removeAccount = value.match(/^Remove (.+) from the account list\?$/);
|
const removeAccount = value.match(/^Remove (.+) from the account list\?$/);
|
||||||
if (removeAccount) return `Soll ${removeAccount[1]} wirklich aus der Accountliste entfernt werden?`;
|
if (removeAccount) return `Soll ${removeAccount[1]} wirklich aus der Accountliste entfernt werden?`;
|
||||||
const resolved = value.match(/^Conflicts resolved: (\d+) overwritten, (\d+) skipped$/);
|
const resolved = value.match(/^Conflicts resolved: (\d+) overwritten, (\d+) skipped$/);
|
||||||
|
|||||||
@@ -75,9 +75,10 @@ export interface AccountWorkspaceActions {
|
|||||||
onEdit: (rowId: string) => void;
|
onEdit: (rowId: string) => void;
|
||||||
onContextMenu: (rowId: string, x: number, y: number) => void;
|
onContextMenu: (rowId: string, x: number, y: number) => void;
|
||||||
onCopyIdentity: (label: "Benutzername" | "E-Mail", value: string) => void;
|
onCopyIdentity: (label: "Benutzername" | "E-Mail", value: string) => void;
|
||||||
onAdd: () => void;
|
onAdd: () => void;
|
||||||
onRemoveSelected: () => void;
|
onRemoveSelected: () => void;
|
||||||
onCheckAll: () => void;
|
onCheckActive: () => void;
|
||||||
|
onCheckAll: () => void;
|
||||||
onSetAllEnabled?: (enabled: boolean) => void;
|
onSetAllEnabled?: (enabled: boolean) => void;
|
||||||
onStatusSort?: () => void;
|
onStatusSort?: () => void;
|
||||||
onMoveProvider?: (index: number, direction: -1 | 1) => void;
|
onMoveProvider?: (index: number, direction: -1 | 1) => void;
|
||||||
@@ -336,7 +337,8 @@ function AccountOverview({ model, actions }: AccountWorkspaceProps): ReactElemen
|
|||||||
<div>
|
<div>
|
||||||
<button className="settings-button settings-button-secondary" disabled={model.busy} onClick={actions.onAdd} type="button">+ Hinzufügen</button>
|
<button className="settings-button settings-button-secondary" disabled={model.busy} onClick={actions.onAdd} type="button">+ Hinzufügen</button>
|
||||||
<button className="settings-button settings-button-secondary" disabled={model.busy || model.selectedIds.length === 0} onClick={actions.onRemoveSelected} type="button">− Entfernen</button>
|
<button className="settings-button settings-button-secondary" disabled={model.busy || model.selectedIds.length === 0} onClick={actions.onRemoveSelected} type="button">− Entfernen</button>
|
||||||
<button className="settings-button settings-button-secondary" disabled={model.busy} onClick={actions.onCheckAll} type="button">↻ Aktualisieren</button>
|
<button className="settings-button settings-button-secondary" disabled={model.busy} onClick={actions.onCheckActive} title="Prüft nur aktivierte Accounts." type="button">↻ Aktive aktualisieren</button>
|
||||||
|
<button className="settings-button settings-button-secondary" disabled={model.busy} onClick={actions.onCheckAll} title="Prüft alle angelegten Accounts, auch deaktivierte." type="button">↻ Alle aktualisieren</button>
|
||||||
</div>
|
</div>
|
||||||
<span>{model.rows.length} {model.rows.length === 1 ? "Account" : "Accounts"}</span>
|
<span>{model.rows.length} {model.rows.length === 1 ? "Account" : "Accounts"}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type {
|
import type {
|
||||||
AddLinksPayload,
|
AddLinksPayload,
|
||||||
|
AccountCheckScope,
|
||||||
AccountCommandResult,
|
AccountCommandResult,
|
||||||
AccountCredentialCheckInput,
|
AccountCredentialCheckInput,
|
||||||
AccountCreateCommand,
|
AccountCreateCommand,
|
||||||
@@ -100,7 +101,7 @@ export interface ElectronApi {
|
|||||||
importBestDebridCookies: () => Promise<number>;
|
importBestDebridCookies: () => Promise<number>;
|
||||||
getAllDebridHostInfo: () => Promise<AllDebridHostInfo>;
|
getAllDebridHostInfo: () => Promise<AllDebridHostInfo>;
|
||||||
getDebridLinkHostLimits: () => Promise<DebridLinkHostLimitInfo[]>;
|
getDebridLinkHostLimits: () => Promise<DebridLinkHostLimitInfo[]>;
|
||||||
checkDebridAccounts: () => Promise<DebridAccountStatus[]>;
|
checkDebridAccounts: (scope?: AccountCheckScope) => Promise<DebridAccountStatus[]>;
|
||||||
checkAccountCredentials: (input: AccountCredentialCheckInput) => Promise<DebridAccountStatus>;
|
checkAccountCredentials: (input: AccountCredentialCheckInput) => Promise<DebridAccountStatus>;
|
||||||
retryExtraction: (packageId: string) => Promise<void>;
|
retryExtraction: (packageId: string) => Promise<void>;
|
||||||
extractNow: (packageId: string) => Promise<void>;
|
extractNow: (packageId: string) => Promise<void>;
|
||||||
|
|||||||
@@ -174,6 +174,8 @@ export interface AppSettings {
|
|||||||
scheduledStartEpochMs: number;
|
scheduledStartEpochMs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AccountCheckScope = "active" | "all";
|
||||||
|
|
||||||
export type RendererAccountKind =
|
export type RendererAccountKind =
|
||||||
| "realdebrid-api"
|
| "realdebrid-api"
|
||||||
| "realdebrid-web"
|
| "realdebrid-web"
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
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 } from "../src/main/account-check";
|
||||||
import type { MegaDebridAccountEntry } from "../src/shared/mega-debrid-accounts";
|
import type { MegaDebridAccountEntry } from "../src/shared/mega-debrid-accounts";
|
||||||
import 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 { getMegaDebridAccountId } from "../src/shared/mega-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" };
|
||||||
@@ -137,7 +139,7 @@ describe("checkRealDebridAccount", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("checkAllDebridAccounts", () => {
|
describe("checkAllDebridAccounts", () => {
|
||||||
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);
|
||||||
@@ -164,7 +166,7 @@ describe("checkAllDebridAccounts", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("checks every configured mega account + debrid-link key", async () => {
|
it("checks every configured mega account + debrid-link key", async () => {
|
||||||
const futureSec = Math.floor(Date.now() / 1000) + 1000;
|
const futureSec = Math.floor(Date.now() / 1000) + 1000;
|
||||||
vi.stubGlobal("fetch", vi.fn(async (url: string) => {
|
vi.stubGlobal("fetch", vi.fn(async (url: string) => {
|
||||||
if (String(url).includes("mega-debrid")) {
|
if (String(url).includes("mega-debrid")) {
|
||||||
@@ -183,8 +185,46 @@ describe("checkAllDebridAccounts", () => {
|
|||||||
expect(result).toHaveLength(5);
|
expect(result).toHaveLength(5);
|
||||||
expect(result.filter((r) => r.provider === "megadebrid")).toHaveLength(2);
|
expect(result.filter((r) => r.provider === "megadebrid")).toHaveLength(2);
|
||||||
expect(result.filter((r) => r.provider === "debridlink")).toHaveLength(3);
|
expect(result.filter((r) => r.provider === "debridlink")).toHaveLength(3);
|
||||||
expect(result.every((r) => r.valid)).toBe(true);
|
expect(result.every((r) => r.valid)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("checks only enabled accounts in active scope and every account in all scope", async () => {
|
||||||
|
const megaCredentials = [
|
||||||
|
"one@example.test:pw1",
|
||||||
|
"two@example.test:pw2",
|
||||||
|
"three@example.test:pw3",
|
||||||
|
"four@example.test:pw4"
|
||||||
|
].join("\n");
|
||||||
|
const settings: AppSettings = {
|
||||||
|
...defaultSettings(),
|
||||||
|
realDebridUseWebLogin: true,
|
||||||
|
megaCredentials,
|
||||||
|
megaDebridWebCredentials: megaCredentials,
|
||||||
|
megaDebridWebEnabled: true,
|
||||||
|
debridLinkApiKeys: "disabled-debrid-link-key",
|
||||||
|
debridLinkDisabledKeyIds: [getDebridLinkApiKeyId("disabled-debrid-link-key")],
|
||||||
|
megaDebridWebDisabledAccountIds: [
|
||||||
|
getMegaDebridAccountId("one@example.test"),
|
||||||
|
getMegaDebridAccountId("two@example.test"),
|
||||||
|
getMegaDebridAccountId("three@example.test"),
|
||||||
|
getMegaDebridAccountId("four@example.test")
|
||||||
|
]
|
||||||
|
};
|
||||||
|
const fetchMock = vi.fn(async () => ({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
text: async () => JSON.stringify({ response_code: "ok", token: "t", vip_end: "4102444800" })
|
||||||
|
}));
|
||||||
|
vi.stubGlobal("fetch", fetchMock as unknown as typeof fetch);
|
||||||
|
const probe = vi.fn(async () => ({ valid: true, isPremium: true, username: "rd-user" }));
|
||||||
|
|
||||||
|
const active = await checkAllDebridAccounts(settings, undefined, probe, "active");
|
||||||
|
const all = await checkAllDebridAccounts(settings, undefined, probe, "all");
|
||||||
|
|
||||||
|
expect(active.map((status) => status.accountId)).toEqual([REAL_DEBRID_STATUS_ID]);
|
||||||
|
expect(all).toHaveLength(6);
|
||||||
|
expect(fetchMock).toHaveBeenCalledTimes(5);
|
||||||
|
});
|
||||||
|
|
||||||
it("caps concurrency (never more than 4 in flight) and preserves result order", async () => {
|
it("caps concurrency (never more than 4 in flight) and preserves result order", async () => {
|
||||||
let inFlight = 0;
|
let inFlight = 0;
|
||||||
|
|||||||
@@ -61,4 +61,16 @@ describe("account preload contract", () => {
|
|||||||
IPC_CHANNELS.DELETE_ACCOUNT
|
IPC_CHANNELS.DELETE_ACCOUNT
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("forwards the selected bulk account-check scope", async () => {
|
||||||
|
electron.invoke.mockResolvedValue([]);
|
||||||
|
|
||||||
|
await electron.api?.checkDebridAccounts("active");
|
||||||
|
await electron.api?.checkDebridAccounts("all");
|
||||||
|
|
||||||
|
expect(electron.invoke.mock.calls).toEqual([
|
||||||
|
[IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS, "active"],
|
||||||
|
[IPC_CHANNELS.CHECK_DEBRID_ACCOUNTS, "all"]
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -188,6 +188,8 @@ describe("renderer localization", () => {
|
|||||||
["Zwischenablage: 3 Link(s) erkannt", "Clipboard: 3 link(s) detected"],
|
["Zwischenablage: 3 Link(s) erkannt", "Clipboard: 3 link(s) detected"],
|
||||||
["2/4 API-Keys deaktiviert.", "2/4 API keys disabled."],
|
["2/4 API-Keys deaktiviert.", "2/4 API keys disabled."],
|
||||||
["Account-Check: 3/4 Login gültig, 2 mit Premium.", "Account check: 3/4 logins valid, 2 with premium."],
|
["Account-Check: 3/4 Login gültig, 2 mit Premium.", "Account check: 3/4 logins valid, 2 with premium."],
|
||||||
|
["Aktive Accounts: 1/1 Login gültig, 1 mit Premium.", "Active accounts: 1/1 logins valid, 1 with premium."],
|
||||||
|
["Alle Accounts: 2/5 Login gültig, 2 mit Premium.", "All accounts: 2/5 logins valid, 2 with premium."],
|
||||||
["Soll RapidGator wirklich aus der Accountliste entfernt werden?", "Remove RapidGator from the account list?"],
|
["Soll RapidGator wirklich aus der Accountliste entfernt werden?", "Remove RapidGator from the account list?"],
|
||||||
["Konflikte gelöst: 2 überschrieben, 3 übersprungen", "Conflicts resolved: 2 overwritten, 3 skipped"],
|
["Konflikte gelöst: 2 überschrieben, 3 übersprungen", "Conflicts resolved: 2 overwritten, 3 skipped"],
|
||||||
["DLC importiert: 2 Paket(e), 5 Link(s)", "DLC imported: 2 package(s), 5 link(s)"],
|
["DLC importiert: 2 Paket(e), 5 Link(s)", "DLC imported: 2 package(s), 5 link(s)"],
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import { buildAccountAddFields, createAccountDialogState } from "../src/renderer
|
|||||||
import { buildAccountReplaceCommand, createAccountEditState, type AccountEditTarget } from "../src/renderer/account-edit";
|
import { buildAccountReplaceCommand, createAccountEditState, type AccountEditTarget } from "../src/renderer/account-edit";
|
||||||
import {
|
import {
|
||||||
buildBulkAccountEnabledState,
|
buildBulkAccountEnabledState,
|
||||||
buildConfiguredProviderOrder
|
buildConfiguredProviderOrder,
|
||||||
|
resolveAccountStatusState,
|
||||||
|
runOptimisticAccountUpdate
|
||||||
} from "../src/renderer/account-ui";
|
} from "../src/renderer/account-ui";
|
||||||
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";
|
||||||
@@ -304,6 +306,7 @@ function workspaceActions(overrides: Partial<AccountWorkspaceActions> = {}): Acc
|
|||||||
onCopyIdentity: () => {},
|
onCopyIdentity: () => {},
|
||||||
onAdd: () => {},
|
onAdd: () => {},
|
||||||
onRemoveSelected: () => {},
|
onRemoveSelected: () => {},
|
||||||
|
onCheckActive: () => {},
|
||||||
onCheckAll: () => {},
|
onCheckAll: () => {},
|
||||||
...overrides
|
...overrides
|
||||||
};
|
};
|
||||||
@@ -727,6 +730,15 @@ describe("account workspace", () => {
|
|||||||
expect(html).not.toContain("role=\"toolbar\"");
|
expect(html).not.toContain("role=\"toolbar\"");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("offers separate checks for active accounts and every configured account", () => {
|
||||||
|
const html = renderToStaticMarkup(<AccountWorkspace actions={workspaceActions()} model={workspaceModel()} />);
|
||||||
|
|
||||||
|
expect(html).toContain("Aktive aktualisieren");
|
||||||
|
expect(html).toContain("Alle aktualisieren");
|
||||||
|
expect(html).toContain('title="Prüft nur aktivierte Accounts."');
|
||||||
|
expect(html).toContain('title="Prüft alle angelegten Accounts, auch deaktivierte."');
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps row selection, enable toggles, edit and context actions separate", () => {
|
it("keeps row selection, enable toggles, edit and context actions separate", () => {
|
||||||
const calls: string[] = [];
|
const calls: string[] = [];
|
||||||
const tree = AccountWorkspace({
|
const tree = AccountWorkspace({
|
||||||
@@ -946,6 +958,34 @@ describe("account workspace", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("settings App integration", () => {
|
describe("settings App integration", () => {
|
||||||
|
it("shows a failed all-account check even when the account is disabled", () => {
|
||||||
|
expect(resolveAccountStatusState(true, { valid: false, isPremium: false })).toBe("invalid");
|
||||||
|
expect(resolveAccountStatusState(true, { valid: true, isPremium: true })).toBe("disabled");
|
||||||
|
expect(resolveAccountStatusState(false, undefined)).toBe("unchecked");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies account switches before persistence settles and rolls back failed saves", async () => {
|
||||||
|
const events: string[] = [];
|
||||||
|
let resolvePersist: (value: string) => void = () => { throw new Error("persist resolver missing"); };
|
||||||
|
const pending = runOptimisticAccountUpdate(
|
||||||
|
() => events.push("apply"),
|
||||||
|
() => new Promise<string>((resolve) => { resolvePersist = resolve; }),
|
||||||
|
() => events.push("rollback")
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(events).toEqual(["apply"]);
|
||||||
|
resolvePersist("saved");
|
||||||
|
await expect(pending).resolves.toBe("saved");
|
||||||
|
expect(events).toEqual(["apply"]);
|
||||||
|
|
||||||
|
await expect(runOptimisticAccountUpdate(
|
||||||
|
() => events.push("apply-failed"),
|
||||||
|
async () => { throw new Error("save failed"); },
|
||||||
|
() => events.push("rollback")
|
||||||
|
)).rejects.toThrow("save failed");
|
||||||
|
expect(events.slice(-2)).toEqual(["apply-failed", "rollback"]);
|
||||||
|
});
|
||||||
|
|
||||||
it("keeps new Mega-Debrid credentials empty and never exposes stored accounts as an API key", () => {
|
it("keeps new Mega-Debrid credentials empty and never exposes stored accounts as an API key", () => {
|
||||||
const settings = {
|
const settings = {
|
||||||
...defaultSettings(),
|
...defaultSettings(),
|
||||||
@@ -974,7 +1014,7 @@ describe("settings App integration", () => {
|
|||||||
|
|
||||||
it("keeps unchecked single accounts honest without a positive status", () => {
|
it("keeps unchecked single accounts honest without a positive status", () => {
|
||||||
const block = sourceBlock(appSource, "const accountSources", "const selectedAccountViewId");
|
const block = sourceBlock(appSource, "const accountSources", "const selectedAccountViewId");
|
||||||
expect(block).toMatch(/:\s*!checkedStatus\s*\?\s*"unchecked"/s);
|
expect(block).toContain("resolveAccountStatusState(row.disabled, checkedStatus)");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("stores only the stable account row id in context-menu state", () => {
|
it("stores only the stable account row id in context-menu state", () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user