diff --git a/src/main/debrid.ts b/src/main/debrid.ts index 094f786..3940613 100644 --- a/src/main/debrid.ts +++ b/src/main/debrid.ts @@ -59,6 +59,23 @@ export function resetDebridLinkRuntimeStateForTests(): void { debridLinkKeyRuntimeStatuses.clear(); } +/** Drop all Debrid-Link cooldown/runtime entries for key IDs that are no + * longer in the active key set. Called when settings change so removed + * keys don't keep blocking the system if they're re-added later. */ +export function pruneDebridLinkRuntimeStateForKeys(activeKeyIds: Set): void { + for (const keyId of debridLinkKeyCooldowns.keys()) { + if (!activeKeyIds.has(keyId)) { + debridLinkKeyCooldowns.delete(keyId); + debridLinkKeyCooldownDetails.delete(keyId); + } + } + for (const keyId of debridLinkKeyRuntimeStatuses.keys()) { + if (!activeKeyIds.has(keyId)) { + debridLinkKeyRuntimeStatuses.delete(keyId); + } + } +} + /** Periodic cleanup of expired Debrid-Link cooldown/runtime entries. * Without this, module-level Maps grow unbounded over 24/7 operation. * Removes entries whose cooldown expired more than 1 hour ago. */ @@ -1535,6 +1552,33 @@ class MegaDebridClient { /** Per-account pending connect deduplication: login (lowercase) → promise */ private static pendingConnects = new Map>(); + /** Clear cached tokens for accounts whose login is no longer in the given set. + * Called when settings change so removed accounts don't keep stale tokens. */ + public static pruneCachedTokensNotIn(activeLogins: Iterable): void { + const keep = new Set(); + for (const login of activeLogins) { + keep.add(String(login || "").toLowerCase()); + } + for (const login of MegaDebridClient.cachedApiTokens.keys()) { + if (!keep.has(login)) { + MegaDebridClient.cachedApiTokens.delete(login); + } + } + for (const login of MegaDebridClient.pendingConnects.keys()) { + if (!keep.has(login)) { + MegaDebridClient.pendingConnects.delete(login); + } + } + } + + /** Force-clear the API token for a specific login (e.g. when its password + * changes — same login, but cached token is now invalid for new password). */ + public static clearCachedApiToken(login: string): void { + const key = String(login || "").toLowerCase(); + MegaDebridClient.cachedApiTokens.delete(key); + MegaDebridClient.pendingConnects.delete(key); + } + public constructor(login: string, password: string, mode: "api" | "web", allowApiFallback: boolean, megaWebUnrestrict?: MegaWebUnrestrictor) { this.login = login; this.password = password; @@ -3065,7 +3109,53 @@ export class DebridService { } public setSettings(next: AppSettings): void { + const prev = this.settings; this.settings = cloneSettings(next); + + // Invalidate cached provider clients whose credentials/keys changed. + // Without this, switching API keys or session-cookie-bound accounts + // (LinkSnappy, Ddownload) would keep using the previous Client instance + // — which holds the OLD session cookies — until the app is restarted. + if (prev.debridLinkApiKeys !== next.debridLinkApiKeys) { + this.cachedDebridLinkClient = null; + this.cachedDebridLinkKey = ""; + } + if (prev.linkSnappyLogin !== next.linkSnappyLogin || prev.linkSnappyPassword !== next.linkSnappyPassword) { + this.cachedLinkSnappyClient = null; + this.cachedLinkSnappyKey = ""; + } + if (prev.ddownloadLogin !== next.ddownloadLogin || prev.ddownloadPassword !== next.ddownloadPassword) { + this.cachedDdownloadClient = null; + this.cachedDdownloadKey = ""; + } + + // Mega-Debrid token cache (static, module-level): tokens are keyed by + // login (lowercase). When credentials change, drop tokens for logins + // that are no longer in the active account list, AND force-clear any + // login whose password changed. Otherwise stale tokens linger up to + // 20 minutes and the new credentials won't be tried until the cached + // token starts returning 401/403. + const prevAccounts = parseMegaDebridAccounts(prev.megaCredentials || "", prev.megaPassword || ""); + const nextAccounts = parseMegaDebridAccounts(next.megaCredentials || "", next.megaPassword || ""); + const nextLogins = new Set(); + const nextPasswordByLogin = new Map(); + for (const acc of nextAccounts) { + nextLogins.add(acc.login.toLowerCase()); + nextPasswordByLogin.set(acc.login.toLowerCase(), acc.password); + } + // Drop tokens for logins no longer present + MegaDebridClient.pruneCachedTokensNotIn(nextLogins); + // For logins still present but with a changed password, force-clear the token + for (const prevAcc of prevAccounts) { + const loginKey = prevAcc.login.toLowerCase(); + if (nextLogins.has(loginKey) && nextPasswordByLogin.get(loginKey) !== prevAcc.password) { + MegaDebridClient.clearCachedApiToken(prevAcc.login); + } + } + // Also prune module-level Debrid-Link cooldowns for keys that no longer exist — + // otherwise a key removed and re-added later would still show its old cooldown. + const nextDebridLinkKeyIds = new Set(parseDebridLinkApiKeys(next.debridLinkApiKeys || "").map((entry) => entry.id)); + pruneDebridLinkRuntimeStateForKeys(nextDebridLinkKeyIds); } private getDebridLinkClient(apiKeysRaw: string): DebridLinkClient { diff --git a/src/main/download-manager.ts b/src/main/download-manager.ts index 649801d..16c3945 100644 --- a/src/main/download-manager.ts +++ b/src/main/download-manager.ts @@ -1987,6 +1987,37 @@ export class DownloadManager extends EventEmitter { this.hybridFailedArchives.clear(); } + // When account credentials change, clear the provider-failure circuit-breaker + // for affected providers. Otherwise a freshly added account would inherit + // the cooldown that the previous (now removed) account had triggered, and + // the user would be confused why "their new account doesn't work right away". + const credChanges: Array<{ prev: string; next: string; providers: string[] }> = [ + { prev: previous.token || "", next: next.token || "", providers: ["realdebrid"] }, + { prev: previous.allDebridToken || "", next: next.allDebridToken || "", providers: ["alldebrid"] }, + { prev: previous.bestDebridApiKey || "", next: next.bestDebridApiKey || "", providers: ["bestdebrid"] }, + { prev: previous.debridLinkApiKeys || "", next: next.debridLinkApiKeys || "", providers: ["debridlink"] }, + { prev: previous.linkSnappyLogin + "|" + previous.linkSnappyPassword, next: next.linkSnappyLogin + "|" + next.linkSnappyPassword, providers: ["linksnappy"] }, + { prev: previous.ddownloadLogin + "|" + previous.ddownloadPassword, next: next.ddownloadLogin + "|" + next.ddownloadPassword, providers: ["ddownload"] }, + { prev: previous.megaCredentials + "|" + previous.megaPassword, next: next.megaCredentials + "|" + next.megaPassword, providers: ["megadebrid", "megadebrid-api", "megadebrid-web"] } + ]; + let clearedProviderFailures = 0; + for (const change of credChanges) { + if (change.prev === change.next) continue; + for (const provider of change.providers) { + // Provider failure keys are sometimes "provider" alone, sometimes "provider:hoster". + // Clear all entries that start with the provider name. + for (const key of [...this.providerFailures.keys()]) { + if (key === provider || key.startsWith(`${provider}:`)) { + this.providerFailures.delete(key); + clearedProviderFailures += 1; + } + } + } + } + if (clearedProviderFailures > 0) { + logger.info(`Settings-Update: ${clearedProviderFailures} Provider-Failure(s) gecleart wegen geaenderter Credentials`); + } + this.resolveExistingQueuedOpaqueFilenames(); void this.cleanupExistingExtractedArchives().catch((err) => logger.warn(`cleanupExistingExtractedArchives Fehler (setSettings): ${compactErrorText(err)}`)); if (next.completedCleanupPolicy !== "never") {