fix(realdebrid): persist web status and report relevant provider errors
Check Real-Debrid API and browser sessions through the account status flow, retain the service status across settings updates, and refresh the account row when a browser login is detected. Exclude unavailable providers that were never attempted from conversion failures and prevent aggregated fallback text from being mislabeled as a Debrid-Link terminal error. Bump the development version to 2.0.37 and add focused regression coverage.
This commit is contained in:
+122
-9
@@ -5,10 +5,24 @@ import { logger } from "./logger";
|
||||
import { compactErrorText } from "./utils";
|
||||
|
||||
const MEGA_DEBRID_API = "https://www.mega-debrid.eu/api.php";
|
||||
const DEBRID_LINK_API = "https://debrid-link.com/api/v2";
|
||||
const DEBRID_LINK_API = "https://debrid-link.com/api/v2";
|
||||
const REAL_DEBRID_USER_API = "https://api.real-debrid.com/rest/1.0/user";
|
||||
const CHECK_USER_AGENT =
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0 Safari/537.36";
|
||||
const CHECK_TIMEOUT_MS = 20000;
|
||||
const CHECK_TIMEOUT_MS = 20000;
|
||||
|
||||
export const REAL_DEBRID_STATUS_ID = "svc-realdebrid";
|
||||
|
||||
export interface RealDebridSessionProbeResult {
|
||||
valid: boolean;
|
||||
isPremium?: boolean;
|
||||
premiumUntilMs?: number | null;
|
||||
username?: string;
|
||||
email?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export type RealDebridSessionProbe = (signal?: AbortSignal) => Promise<RealDebridSessionProbeResult>;
|
||||
|
||||
function timeoutSignal(signal: AbortSignal | undefined, ms: number): AbortSignal {
|
||||
const timeout = AbortSignal.timeout(ms);
|
||||
@@ -24,7 +38,7 @@ function parseJsonSafe(text: string): Record<string, unknown> | null {
|
||||
}
|
||||
}
|
||||
|
||||
function formatRemaining(premiumUntilMs: number | null, now: number): string {
|
||||
function formatRemaining(premiumUntilMs: number | null, now: number): string {
|
||||
if (premiumUntilMs == null) {
|
||||
return "Premium-Status unbekannt";
|
||||
}
|
||||
@@ -41,7 +55,102 @@ function formatRemaining(premiumUntilMs: number | null, now: number): string {
|
||||
}
|
||||
const hours = Math.max(1, Math.floor(remainingMs / (60 * 60 * 1000)));
|
||||
return `Premium noch ${hours} Std`;
|
||||
}
|
||||
}
|
||||
|
||||
function maskSecret(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
if (trimmed.length <= 6) {
|
||||
return "*".repeat(trimmed.length);
|
||||
}
|
||||
return `${trimmed.slice(0, 3)}${"*".repeat(Math.max(4, trimmed.length - 6))}${trimmed.slice(-3)}`;
|
||||
}
|
||||
|
||||
export async function checkRealDebridAccount(
|
||||
settings: AppSettings,
|
||||
signal?: AbortSignal,
|
||||
now = Date.now(),
|
||||
probeWebSession?: RealDebridSessionProbe
|
||||
): Promise<DebridAccountStatus> {
|
||||
const token = String(settings.token || "").trim();
|
||||
const useWebLogin = Boolean(settings.realDebridUseWebLogin);
|
||||
const base: DebridAccountStatus = {
|
||||
accountId: REAL_DEBRID_STATUS_ID,
|
||||
provider: "realdebrid",
|
||||
label: "Real-Debrid",
|
||||
maskedLogin: useWebLogin ? "Browser-Login" : maskSecret(token),
|
||||
valid: false,
|
||||
isPremium: false,
|
||||
premiumUntilMs: null,
|
||||
message: "",
|
||||
checkedAt: now
|
||||
};
|
||||
|
||||
if (useWebLogin) {
|
||||
if (!probeWebSession) {
|
||||
return { ...base, message: "Browser-Sitzung ist nicht prüfbar" };
|
||||
}
|
||||
try {
|
||||
const result = await probeWebSession(signal);
|
||||
if (!result.valid) {
|
||||
return { ...base, message: result.message || "Browser-Sitzung abgelaufen" };
|
||||
}
|
||||
const premiumUntilMs = typeof result.premiumUntilMs === "number" ? result.premiumUntilMs : null;
|
||||
return {
|
||||
...base,
|
||||
valid: true,
|
||||
isPremium: Boolean(result.isPremium),
|
||||
premiumUntilMs,
|
||||
email: String(result.email || result.username || "").trim() || undefined,
|
||||
message: result.message || (result.isPremium ? formatRemaining(premiumUntilMs, now) : "Kein Premium (Free)")
|
||||
};
|
||||
} catch (error) {
|
||||
const errText = compactErrorText(error);
|
||||
const aborted = signal?.aborted || /aborted/i.test(errText);
|
||||
return { ...base, message: aborted ? "Prüfung abgebrochen" : `Prüfung fehlgeschlagen: ${errText}` };
|
||||
}
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
return { ...base, message: "Kein API-Token hinterlegt" };
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(REAL_DEBRID_USER_API, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"User-Agent": CHECK_USER_AGENT
|
||||
},
|
||||
signal: timeoutSignal(signal, CHECK_TIMEOUT_MS)
|
||||
});
|
||||
const text = await response.text();
|
||||
const payload = parseJsonSafe(text);
|
||||
if (!response.ok || !payload) {
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
return { ...base, message: "Ungültiger API-Token" };
|
||||
}
|
||||
return { ...base, message: `Prüfung fehlgeschlagen (HTTP ${response.status})` };
|
||||
}
|
||||
const expiration = Date.parse(String(payload.expiration || ""));
|
||||
const premiumUntilMs = Number.isFinite(expiration) ? expiration : null;
|
||||
const isPremium = String(payload.type || "").toLowerCase() === "premium"
|
||||
&& (premiumUntilMs == null || premiumUntilMs > now);
|
||||
return {
|
||||
...base,
|
||||
valid: true,
|
||||
isPremium,
|
||||
premiumUntilMs,
|
||||
email: String(payload.email || payload.username || "").trim() || undefined,
|
||||
message: isPremium ? formatRemaining(premiumUntilMs, now) : "Kein Premium (Free)"
|
||||
};
|
||||
} catch (error) {
|
||||
const errText = compactErrorText(error);
|
||||
const aborted = signal?.aborted || /aborted/i.test(errText);
|
||||
return { ...base, message: aborted ? "Prüfung abgebrochen" : `Prüfung fehlgeschlagen: ${errText}` };
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkMegaDebridAccount(
|
||||
account: MegaDebridAccountEntry,
|
||||
@@ -158,15 +267,19 @@ export async function checkDebridLinkKey(
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkAllDebridAccounts(
|
||||
settings: AppSettings,
|
||||
signal?: AbortSignal
|
||||
): Promise<DebridAccountStatus[]> {
|
||||
export async function checkAllDebridAccounts(
|
||||
settings: AppSettings,
|
||||
signal?: AbortSignal,
|
||||
probeRealDebridWebSession?: RealDebridSessionProbe
|
||||
): Promise<DebridAccountStatus[]> {
|
||||
const now = Date.now();
|
||||
const megaAccounts = parseMegaDebridAccounts(settings.megaCredentials || "", settings.megaPassword || "");
|
||||
const debridLinkKeys = parseDebridLinkApiKeys(settings.debridLinkApiKeys || "");
|
||||
|
||||
const taskFns: Array<() => Promise<DebridAccountStatus>> = [
|
||||
const taskFns: Array<() => Promise<DebridAccountStatus>> = [
|
||||
...(settings.realDebridUseWebLogin || String(settings.token || "").trim()
|
||||
? [() => checkRealDebridAccount(settings, signal, now, probeRealDebridWebSession)]
|
||||
: []),
|
||||
...megaAccounts.map((account) => () => checkMegaDebridAccount(account, signal, now)),
|
||||
...debridLinkKeys.map((key) => () => checkDebridLinkKey(key, signal, now))
|
||||
];
|
||||
|
||||
@@ -121,7 +121,11 @@ export function validateAccountCredentialCheckInput(value: unknown): AccountCred
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) invalid();
|
||||
const raw = value as Record<string, unknown>;
|
||||
if (Object.keys(raw).some((key) => !new Set(["kind", "accountId", "identity", "secret"]).has(key))) invalid();
|
||||
if (raw.kind !== "megadebrid-api" && raw.kind !== "megadebrid-web" && raw.kind !== "debridlink-api") invalid();
|
||||
if (raw.kind !== "realdebrid-api"
|
||||
&& raw.kind !== "realdebrid-web"
|
||||
&& raw.kind !== "megadebrid-api"
|
||||
&& raw.kind !== "megadebrid-web"
|
||||
&& raw.kind !== "debridlink-api") invalid();
|
||||
return {
|
||||
kind: raw.kind,
|
||||
accountId: optionalString(raw.accountId, 256),
|
||||
|
||||
@@ -30,7 +30,7 @@ import { importDlcContainers } from "./container";
|
||||
import { APP_VERSION, ONLINE_BACKUP_API_URL } from "./constants";
|
||||
import { DownloadManager } from "./download-manager";
|
||||
import { fetchAllDebridHostInfo, fetchDebridLinkHostLimits } from "./debrid";
|
||||
import { checkAllDebridAccounts, checkDebridLinkKey, checkMegaDebridAccount } from "./account-check";
|
||||
import { checkAllDebridAccounts, checkDebridLinkKey, checkMegaDebridAccount, checkRealDebridAccount } from "./account-check";
|
||||
import { parseMegaDebridAccounts } from "../shared/mega-debrid-accounts";
|
||||
import { getMegaDebridAccountsForMode } from "../shared/mega-debrid-accounts";
|
||||
import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
|
||||
@@ -133,7 +133,10 @@ export class AppController {
|
||||
login: this.settings.megaLogin,
|
||||
password: this.settings.megaPassword
|
||||
}));
|
||||
this.realDebridWebFallback = new RealDebridWebFallback(() => this.settings.rememberToken);
|
||||
this.realDebridWebFallback = new RealDebridWebFallback(
|
||||
() => this.settings.rememberToken,
|
||||
() => { void this.refreshRealDebridWebStatus(); }
|
||||
);
|
||||
this.allDebridWebFallback = new AllDebridWebFallback(() => this.settings.rememberToken);
|
||||
this.bestDebridWebFallback = new BestDebridWebFallback(() => this.settings.rememberToken);
|
||||
this.manager = new DownloadManager(this.settings, session, this.storagePaths, {
|
||||
@@ -532,6 +535,25 @@ export class AppController {
|
||||
|
||||
public async checkAccountCredentials(input: AccountCredentialCheckInput): Promise<DebridAccountStatus> {
|
||||
const redactions = collectAccountStatusRedactionValues(this.settings, input);
|
||||
if (input.kind === "realdebrid-api" || input.kind === "realdebrid-web") {
|
||||
const useWebLogin = input.kind === "realdebrid-web";
|
||||
const settings = input.secret?.trim()
|
||||
? { ...this.settings, token: input.secret.trim(), realDebridUseWebLogin: useWebLogin }
|
||||
: { ...this.settings, realDebridUseWebLogin: useWebLogin };
|
||||
const status = sanitizeDebridAccountStatus(
|
||||
await checkRealDebridAccount(
|
||||
settings,
|
||||
undefined,
|
||||
Date.now(),
|
||||
useWebLogin ? (signal) => this.realDebridWebFallback.probeLoginState(signal) : undefined
|
||||
),
|
||||
redactions
|
||||
);
|
||||
if (!input.secret && useWebLogin === this.settings.realDebridUseWebLogin) {
|
||||
this.manager.applyDebridAccountStatuses([status]);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
if (input.kind === "megadebrid-api" || input.kind === "megadebrid-web") {
|
||||
const mode = input.kind === "megadebrid-web" ? "web" : "api";
|
||||
const account = input.identity?.trim() && input.secret
|
||||
@@ -575,10 +597,26 @@ export class AppController {
|
||||
return this.settings;
|
||||
}
|
||||
|
||||
public async openRealDebridLoginWindow(): Promise<void> {
|
||||
public async openRealDebridLoginWindow(): Promise<void> {
|
||||
this.audit("INFO", "Real-Debrid Login-Fenster geöffnet");
|
||||
await this.realDebridWebFallback.openLoginWindow();
|
||||
}
|
||||
await this.realDebridWebFallback.openLoginWindow();
|
||||
}
|
||||
|
||||
private async refreshRealDebridWebStatus(): Promise<void> {
|
||||
if (!this.settings.realDebridUseWebLogin) {
|
||||
return;
|
||||
}
|
||||
const status = sanitizeDebridAccountStatus(
|
||||
await checkRealDebridAccount(
|
||||
this.settings,
|
||||
undefined,
|
||||
Date.now(),
|
||||
(signal) => this.realDebridWebFallback.probeLoginState(signal)
|
||||
),
|
||||
collectAccountStatusRedactionValues(this.settings)
|
||||
);
|
||||
this.manager.applyDebridAccountStatuses([status]);
|
||||
}
|
||||
|
||||
public async openAllDebridLoginWindow(): Promise<void> {
|
||||
this.audit("INFO", "AllDebrid Login-Fenster geöffnet");
|
||||
@@ -611,7 +649,11 @@ export class AppController {
|
||||
|
||||
public async checkDebridAccounts(): Promise<DebridAccountStatus[]> {
|
||||
const statuses = sanitizeDebridAccountStatuses(
|
||||
await checkAllDebridAccounts(this.settings),
|
||||
await checkAllDebridAccounts(
|
||||
this.settings,
|
||||
undefined,
|
||||
(signal) => this.realDebridWebFallback.probeLoginState(signal)
|
||||
),
|
||||
collectAccountStatusRedactionValues(this.settings)
|
||||
);
|
||||
this.manager.applyDebridAccountStatuses(statuses);
|
||||
|
||||
+9
-8
@@ -3964,8 +3964,8 @@ export class DebridService {
|
||||
}
|
||||
|
||||
let configuredFound = false;
|
||||
let limitReachedFound = false;
|
||||
const attempts: string[] = [];
|
||||
const attempts: string[] = [];
|
||||
const unavailableProviders: string[] = [];
|
||||
|
||||
for (const provider of order) {
|
||||
if (!this.isProviderConfiguredFor(settings, provider)) {
|
||||
@@ -3973,9 +3973,8 @@ export class DebridService {
|
||||
}
|
||||
configuredFound = true;
|
||||
if (this.isProviderDailyLimited(settings, provider)) {
|
||||
limitReachedFound = true;
|
||||
logger.info(`Provider-Kette: ${PROVIDER_LABELS[provider]} uebersprungen (${this.formatProviderLimitMessage(settings, provider)})`);
|
||||
attempts.push(this.formatProviderLimitMessage(settings, provider));
|
||||
logger.info(`Provider-Kette: ${PROVIDER_LABELS[provider]} uebersprungen (${this.formatProviderLimitMessage(settings, provider)})`);
|
||||
unavailableProviders.push(this.formatProviderLimitMessage(settings, provider));
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -4024,9 +4023,11 @@ export class DebridService {
|
||||
if (!configuredFound) {
|
||||
throw new Error("Kein Debrid-Provider konfiguriert");
|
||||
}
|
||||
if (limitReachedFound && attempts.every((entry) => /Tageslimit erreicht$/i.test(entry))) {
|
||||
throw new Error("Alle konfigurierten Provider haben ihr Tageslimit erreicht");
|
||||
}
|
||||
if (attempts.length === 0 && unavailableProviders.length > 0) {
|
||||
throw new Error(unavailableProviders.length === 1
|
||||
? unavailableProviders[0]
|
||||
: `Alle konfigurierten Provider sind nicht verfügbar: ${unavailableProviders.join(" | ")}`);
|
||||
}
|
||||
|
||||
throw new Error(`Unrestrict fehlgeschlagen: ${attempts.join(" | ")}`);
|
||||
}
|
||||
|
||||
@@ -696,11 +696,11 @@ export function parseMegaDebridResetPark(errorText: string): { delayMs: number;
|
||||
return { delayMs, detail: String(match[2] || "").trim() };
|
||||
}
|
||||
|
||||
function parseDebridLinkTerminalFailure(errorText: string): { kind: "invalid_all" | "no_active_key"; detail: string } | null {
|
||||
export function parseDebridLinkTerminalFailure(errorText: string): { kind: "invalid_all" | "no_active_key"; detail: string } | null {
|
||||
const raw = String(errorText || "");
|
||||
const match = raw.match(/debrid_link_(invalid_all|no_active_key):(.*)$/i);
|
||||
if (!match) {
|
||||
if (/debrid-link.+(deaktiviert|ausgeschopft|kein aktiver api-key)/i.test(raw)) {
|
||||
if (/^(?:Error:\s*)?Debrid-Link.+(?:deaktiviert|ausgeschopft|kein aktiver api-key)/i.test(raw)) {
|
||||
return {
|
||||
kind: "no_active_key",
|
||||
detail: raw.trim()
|
||||
|
||||
+91
-16
@@ -7,14 +7,28 @@ import { applyRemoteLoginSecurity, createRemoteLoginWebPreferences, REALDEBRID_L
|
||||
const RD_BASE_URL = "https://real-debrid.com";
|
||||
const RD_LOGIN_URL = RD_BASE_URL;
|
||||
const RD_APITOKEN_URL = `${RD_BASE_URL}/apitoken`;
|
||||
const RD_UNRESTRICT_API = `${API_BASE_URL}/unrestrict/link`;
|
||||
const RD_UNRESTRICT_API = `${API_BASE_URL}/unrestrict/link`;
|
||||
const RD_USER_API = `${API_BASE_URL}/user`;
|
||||
const RD_PERSISTENT_PARTITION = "persist:realdebrid-web";
|
||||
const RD_TRANSIENT_PARTITION = "realdebrid-web";
|
||||
const RD_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36";
|
||||
|
||||
type GenerateOutcome =
|
||||
| { kind: "success"; value: UnrestrictedLink }
|
||||
| { kind: "login_required" };
|
||||
type GenerateOutcome =
|
||||
| { kind: "success"; value: UnrestrictedLink }
|
||||
| { kind: "login_required" };
|
||||
|
||||
export interface RealDebridLoginState {
|
||||
valid: boolean;
|
||||
username: string;
|
||||
email: string;
|
||||
isPremium: boolean;
|
||||
premiumUntilMs: number | null;
|
||||
message: string;
|
||||
}
|
||||
|
||||
function loginFailure(message: string): RealDebridLoginState {
|
||||
return { valid: false, username: "", email: "", isPremium: false, premiumUntilMs: null, message };
|
||||
}
|
||||
|
||||
function abortError(): Error {
|
||||
return new Error("aborted:realdebrid-web");
|
||||
@@ -116,10 +130,13 @@ export class RealDebridWebFallback {
|
||||
|
||||
private cachedTokenAt = 0;
|
||||
|
||||
private getRememberSession: () => boolean;
|
||||
|
||||
public constructor(getRememberSession: () => boolean) {
|
||||
this.getRememberSession = getRememberSession;
|
||||
private getRememberSession: () => boolean;
|
||||
|
||||
private onAuthenticated?: () => void;
|
||||
|
||||
public constructor(getRememberSession: () => boolean, onAuthenticated?: () => void) {
|
||||
this.getRememberSession = getRememberSession;
|
||||
this.onAuthenticated = onAuthenticated;
|
||||
}
|
||||
|
||||
public async unrestrict(link: string, signal?: AbortSignal): Promise<UnrestrictedLink | null> {
|
||||
@@ -138,15 +155,69 @@ export class RealDebridWebFallback {
|
||||
}, overallSignal);
|
||||
}
|
||||
|
||||
public async openLoginWindow(): Promise<void> {
|
||||
public async openLoginWindow(): Promise<void> {
|
||||
const window = await this.ensureLoginWindow();
|
||||
if (window.isMinimized()) {
|
||||
window.restore();
|
||||
}
|
||||
window.show();
|
||||
window.focus();
|
||||
void this.primeTokenFromWindow(window);
|
||||
}
|
||||
void this.primeTokenFromWindow(window);
|
||||
}
|
||||
|
||||
public async probeLoginState(signal?: AbortSignal): Promise<RealDebridLoginState> {
|
||||
let token: string | null = null;
|
||||
try {
|
||||
token = await this.extractApiToken(signal);
|
||||
} catch (error) {
|
||||
if (signal?.aborted) {
|
||||
throw abortError();
|
||||
}
|
||||
return loginFailure(`Sitzung nicht prüfbar: ${String(error)}`);
|
||||
}
|
||||
if (!token) {
|
||||
return loginFailure("Nicht angemeldet");
|
||||
}
|
||||
try {
|
||||
const response = await fetch(RD_USER_API, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"User-Agent": RD_USER_AGENT
|
||||
},
|
||||
signal: withTimeoutSignal(signal, 30_000)
|
||||
});
|
||||
const text = await response.text();
|
||||
if (response.status === 401 || response.status === 403) {
|
||||
this.cachedToken = "";
|
||||
this.cachedTokenAt = 0;
|
||||
return loginFailure("Sitzung abgelaufen");
|
||||
}
|
||||
if (!response.ok) {
|
||||
return loginFailure(`Real-Debrid Web HTTP ${response.status}`);
|
||||
}
|
||||
const payload = parseJson(text);
|
||||
if (!payload) {
|
||||
return loginFailure("Ungültige Antwort von Real-Debrid");
|
||||
}
|
||||
const expiration = Date.parse(String(payload.expiration || ""));
|
||||
const premiumUntilMs = Number.isFinite(expiration) ? expiration : null;
|
||||
const isPremium = String(payload.type || "").toLowerCase() === "premium"
|
||||
&& (premiumUntilMs == null || premiumUntilMs > Date.now());
|
||||
return {
|
||||
valid: true,
|
||||
username: String(payload.username || "").trim(),
|
||||
email: String(payload.email || "").trim(),
|
||||
isPremium,
|
||||
premiumUntilMs,
|
||||
message: isPremium ? "Premium aktiv" : "Kein Premium (Free)"
|
||||
};
|
||||
} catch (error) {
|
||||
if (signal?.aborted) {
|
||||
throw abortError();
|
||||
}
|
||||
return loginFailure(`Sitzung nicht prüfbar: ${String(error)}`);
|
||||
}
|
||||
}
|
||||
|
||||
public async clearSessions(): Promise<void> {
|
||||
this.disposeLoginWindow();
|
||||
@@ -247,11 +318,15 @@ export class RealDebridWebFallback {
|
||||
return window;
|
||||
}
|
||||
|
||||
private rememberToken(token: string): string {
|
||||
this.cachedToken = token;
|
||||
this.cachedTokenAt = Date.now();
|
||||
return token;
|
||||
}
|
||||
private rememberToken(token: string): string {
|
||||
const changed = token !== this.cachedToken;
|
||||
this.cachedToken = token;
|
||||
this.cachedTokenAt = Date.now();
|
||||
if (changed && this.onAuthenticated) {
|
||||
void Promise.resolve().then(() => this.onAuthenticated?.());
|
||||
}
|
||||
return token;
|
||||
}
|
||||
|
||||
private getActiveLoginWindow(): BrowserWindow | null {
|
||||
const window = this.loginWindow;
|
||||
|
||||
@@ -5,7 +5,11 @@ import type { AppSettings } from "../shared/types";
|
||||
export function overlayLiveUsageCounters(target: AppSettings, liveSettings: AppSettings, liveTotalRuntimeMs: number): void {
|
||||
const debridLinkKeyIds = new Set(getDebridLinkApiKeyIds(target.debridLinkApiKeys));
|
||||
const megaAccountIds = new Set(getMegaDebridAccountIds(mergeMegaDebridCredentialPools(target.megaDebridApiCredentials || "", target.megaDebridWebCredentials || "") || target.megaCredentials || "", target.megaPassword || ""));
|
||||
const validAccountIds = new Set([...debridLinkKeyIds, ...megaAccountIds]);
|
||||
const validAccountIds = new Set([
|
||||
...debridLinkKeyIds,
|
||||
...megaAccountIds,
|
||||
...(target.realDebridUseWebLogin || target.token.trim() ? ["svc-realdebrid"] : [])
|
||||
]);
|
||||
target.totalDownloadedAllTime = Math.max(target.totalDownloadedAllTime || 0, liveSettings.totalDownloadedAllTime || 0);
|
||||
target.totalCompletedFilesAllTime = Math.max(target.totalCompletedFilesAllTime || 0, liveSettings.totalCompletedFilesAllTime || 0);
|
||||
target.totalRuntimeAllTimeMs = Math.max(target.totalRuntimeAllTimeMs || 0, liveTotalRuntimeMs);
|
||||
|
||||
+18
-8
@@ -266,12 +266,13 @@ function normalizeNamedByteMap(raw: unknown, allowedKeys: readonly string[]): Re
|
||||
return result;
|
||||
}
|
||||
|
||||
function normalizeDebridAccountStatuses(
|
||||
value: unknown,
|
||||
megaIds: string[],
|
||||
debridLinkIds: string[]
|
||||
): Record<string, DebridAccountStatus> {
|
||||
const allowed = new Set([...megaIds, ...debridLinkIds]);
|
||||
function normalizeDebridAccountStatuses(
|
||||
value: unknown,
|
||||
megaIds: string[],
|
||||
debridLinkIds: string[],
|
||||
realDebridConfigured: boolean
|
||||
): Record<string, DebridAccountStatus> {
|
||||
const allowed = new Set([...megaIds, ...debridLinkIds, ...(realDebridConfigured ? ["svc-realdebrid"] : [])]);
|
||||
const result: Record<string, DebridAccountStatus> = {};
|
||||
if (value && typeof value === "object" && !Array.isArray(value)) {
|
||||
for (const [key, raw] of Object.entries(value as Record<string, unknown>)) {
|
||||
@@ -284,7 +285,11 @@ function normalizeDebridAccountStatuses(
|
||||
}
|
||||
result[key] = {
|
||||
accountId: entry.accountId,
|
||||
provider: entry.provider === "debridlink" ? "debridlink" : "megadebrid",
|
||||
provider: entry.provider === "debridlink"
|
||||
? "debridlink"
|
||||
: entry.provider === "realdebrid"
|
||||
? "realdebrid"
|
||||
: "megadebrid",
|
||||
label: String(entry.label || ""),
|
||||
maskedLogin: String(entry.maskedLogin || ""),
|
||||
valid: Boolean(entry.valid),
|
||||
@@ -570,7 +575,12 @@ export function normalizeSettings(settings: AppSettings): AppSettings {
|
||||
? normalizeNamedByteMap(settings.megaDebridAccountDailyUsageBytes, megaDebridAccountIds)
|
||||
: {},
|
||||
megaDebridAccountTotalUsageBytes: normalizeNamedByteMap(settings.megaDebridAccountTotalUsageBytes, megaDebridAccountIds),
|
||||
debridAccountStatuses: normalizeDebridAccountStatuses(settings.debridAccountStatuses, megaDebridAccountIds, debridLinkApiKeyIds),
|
||||
debridAccountStatuses: normalizeDebridAccountStatuses(
|
||||
settings.debridAccountStatuses,
|
||||
megaDebridAccountIds,
|
||||
debridLinkApiKeyIds,
|
||||
Boolean(settings.realDebridUseWebLogin || asText(settings.token))
|
||||
),
|
||||
providerDailyUsageDay: providerDailyUsageDay === currentUsageDay ? providerDailyUsageDay : currentUsageDay,
|
||||
scheduledStartEpochMs: clampNumber(settings.scheduledStartEpochMs, defaults.scheduledStartEpochMs, 0, Number.MAX_SAFE_INTEGER)
|
||||
};
|
||||
|
||||
@@ -2359,16 +2359,17 @@ export function App(): ReactElement {
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
rows.push({
|
||||
} else {
|
||||
const serviceAccountId = entry.service === "realdebrid" ? "svc-realdebrid" : null;
|
||||
rows.push({
|
||||
rowKey: `svc-${entry.service}`,
|
||||
entry,
|
||||
hosterLabel: entry.serviceLabel,
|
||||
modeLabel: entry.modeLabel,
|
||||
username: getStoredAccountUsername(entry.kind, snapshot.accounts),
|
||||
credentialLabel: getAccountCredentialLabel(entry.kind),
|
||||
accountId: null,
|
||||
checkable: false,
|
||||
accountId: serviceAccountId,
|
||||
checkable: serviceAccountId !== null,
|
||||
disabled: entry.disabled,
|
||||
dailyUsedBytes: entry.dailyUsedBytes,
|
||||
dailyLimitBytes: entry.dailyLimitBytes,
|
||||
@@ -2653,8 +2654,8 @@ export function App(): ReactElement {
|
||||
setAccountCheckBusy(true);
|
||||
try {
|
||||
const statuses = await window.rd.checkDebridAccounts();
|
||||
if (!statuses || statuses.length === 0) {
|
||||
showToast("Keine Mega-Debrid-/Debrid-Link-Accounts zum Prüfen konfiguriert.", 3200);
|
||||
if (!statuses || statuses.length === 0) {
|
||||
showToast("Keine prüfbaren Accounts konfiguriert.", 3200);
|
||||
} else {
|
||||
const valid = statuses.filter((st) => st.valid).length;
|
||||
const premium = statuses.filter((st) => st.isPremium).length;
|
||||
@@ -2941,7 +2942,7 @@ export function App(): ReactElement {
|
||||
|
||||
const checkAccountTableRow = (row: AccountTableRow): void => {
|
||||
setAccountContextMenu(null);
|
||||
if (row.toggleKind === "mega" || row.toggleKind === "dl") {
|
||||
if (row.checkable) {
|
||||
void checkAllAccounts();
|
||||
return;
|
||||
}
|
||||
|
||||
+4
-4
@@ -55,9 +55,9 @@ export interface DownloadStats {
|
||||
runtimeMeasuredAt: number;
|
||||
}
|
||||
|
||||
export interface DebridAccountStatus {
|
||||
accountId: string;
|
||||
provider: "megadebrid" | "debridlink";
|
||||
export interface DebridAccountStatus {
|
||||
accountId: string;
|
||||
provider: DebridProvider;
|
||||
label: string;
|
||||
maskedLogin: string;
|
||||
valid: boolean;
|
||||
@@ -337,7 +337,7 @@ export interface AccountCommandResult {
|
||||
}
|
||||
|
||||
export interface AccountCredentialCheckInput {
|
||||
kind: "megadebrid-api" | "megadebrid-web" | "debridlink-api";
|
||||
kind: "realdebrid-api" | "realdebrid-web" | "megadebrid-api" | "megadebrid-web" | "debridlink-api";
|
||||
accountId?: string;
|
||||
identity?: string;
|
||||
secret?: string;
|
||||
|
||||
Reference in New Issue
Block a user