Files
Multi-Debrid-Downloader/src/main/account-runtime-snapshot.ts
T
Sucukdeluxe a8a5ca4b40 release: prepare v2.0.43
Add sanitized provider and account runtime diagnostics, complete the Multi-Debrid-Downloader product rename, preserve existing application data during migration, update public release verification, and publish the accompanying English documentation and regression coverage.
2026-08-16 21:50:01 +02:00

123 lines
5.3 KiB
TypeScript

import type {
AccountRuntimeEntry,
DebridProvider,
DownloadItem,
RendererAccount
} from "../shared/types";
import { getAccountRuntimeSessionStats } from "./account-runtime";
import { getProviderRuntimeSnapshot, type ProviderRuntimeCooldown } from "./debrid";
const ACTIVE_DOWNLOAD_STATUSES = new Set(["validating", "downloading", "paused", "reconnect_wait"]);
function providerFamily(provider: DebridProvider | null): string {
if (provider === "megadebrid" || provider === "megadebrid-api" || provider === "megadebrid-web") return "megadebrid";
return provider || "";
}
function accountRuntimeKey(provider: DebridProvider, accountId: string): string {
return `${provider}:${accountId}`;
}
function sanitizedCooldownReason(category: string): string {
if (category === "invalid") return "Anmeldung ungültig";
if (category === "rate_limit") return "Rate-Limit aktiv";
if (category === "quota") return "Traffic- oder Kontolimit erreicht";
if (category === "provider_or_link") return "Provider oder Link vorübergehend nicht verfügbar";
return "Vorübergehender Cooldown";
}
function accountCooldown(
account: RendererAccount,
runtime: ReturnType<typeof getProviderRuntimeSnapshot>
): { cooldown: ProviderRuntimeCooldown | null; inFlight: number } {
if (account.provider === "realdebrid") {
const entry = runtime.realDebrid.accounts.find((candidate) => candidate.accountId === account.accountId);
return { cooldown: entry?.cooldown ?? null, inFlight: entry?.inFlight ?? 0 };
}
if (account.provider === "megadebrid-api" || account.provider === "megadebrid-web") {
const mode = account.provider === "megadebrid-api" ? "api" : "web";
const entry = runtime.megaDebrid.accounts.find((candidate) => candidate.key === `${account.accountId}:${mode}`);
return { cooldown: entry?.cooldown ?? null, inFlight: entry?.inFlight ?? 0 };
}
if (account.provider === "debridlink") {
const entry = runtime.debridLink.keys.find((candidate) => candidate.keyId === account.accountId);
return { cooldown: entry?.cooldown ?? null, inFlight: 0 };
}
return { cooldown: null, inFlight: 0 };
}
function attributedAccount(item: DownloadItem, accounts: readonly RendererAccount[]): RendererAccount | null {
if (item.providerAccountId) {
const exact = accounts.filter((account) => account.accountId === item.providerAccountId && account.provider === item.provider);
if (exact.length === 1) return exact[0];
const sameFamily = accounts.filter((account) => account.accountId === item.providerAccountId && providerFamily(account.provider) === providerFamily(item.provider));
if (sameFamily.length === 1) return sameFamily[0];
}
const family = providerFamily(item.provider);
const candidates = accounts.filter((account) => providerFamily(account.provider) === family);
return candidates.length === 1 ? candidates[0] : null;
}
export function createAccountRuntimeEntries(
accounts: readonly RendererAccount[],
items: readonly DownloadItem[],
now = Date.now()
): AccountRuntimeEntry[] {
const providerRuntime = getProviderRuntimeSnapshot(now);
const activeByAccount = new Map<string, { count: number; lastUsedAt: number }>();
for (const item of items) {
if (!ACTIVE_DOWNLOAD_STATUSES.has(item.status)) continue;
const account = attributedAccount(item, accounts);
if (!account) continue;
const key = accountRuntimeKey(account.provider, account.accountId);
const current = activeByAccount.get(key) ?? { count: 0, lastUsedAt: 0 };
activeByAccount.set(key, {
count: current.count + 1,
lastUsedAt: Math.max(current.lastUsedAt, item.updatedAt || item.createdAt || now)
});
}
return accounts.map((account) => {
const stats = getAccountRuntimeSessionStats(account.provider, account.accountId);
const active = activeByAccount.get(accountRuntimeKey(account.provider, account.accountId)) ?? { count: 0, lastUsedAt: 0 };
const { cooldown, inFlight } = accountCooldown(account, providerRuntime);
const dailyLimitReached = account.dailyLimitBytes > 0 && account.dailyUsageBytes >= account.dailyLimitBytes;
const invalid = account.status?.valid === false;
let state: AccountRuntimeEntry["state"] = "ready";
let reason = "Bereit";
if (!account.enabled) {
state = "disabled";
reason = "Account deaktiviert";
} else if (dailyLimitReached) {
state = "daily_limit";
reason = "Tageslimit erreicht";
} else if (cooldown && cooldown.remainingMs > 0) {
state = "cooldown";
reason = sanitizedCooldownReason(cooldown.category);
} else if (active.count > 0) {
state = "active";
reason = active.count === 1 ? "1 aktiver Download" : `${active.count} aktive Downloads`;
} else if (inFlight > 0) {
state = "checking";
reason = "Link wird geprüft";
} else if (invalid) {
state = "invalid";
reason = "Accountprüfung fehlgeschlagen";
}
return {
accountId: account.accountId,
provider: account.provider,
state,
reason,
activeDownloads: active.count,
inFlight,
attempts: stats.attempts,
successes: stats.successes,
failures: stats.failures,
lastUsedAt: Math.max(stats.lastUsedAt ?? 0, active.lastUsedAt) || null,
cooldownUntil: cooldown && cooldown.remainingMs > 0 ? cooldown.untilMs : null,
dailyUsageBytes: account.dailyUsageBytes
};
});
}