fix(accounts): restore Real-Debrid start availability
Keep concrete account pools authoritative when fully disabled and clear stale transient cooldown state when an account is explicitly re-enabled, while preserving configured daily limits and usage.
This commit is contained in:
@@ -4,6 +4,14 @@ All notable changes to Multi-Debrid Downloader are documented in this file.
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2.0.45] - 2026-08-19
|
||||||
|
|
||||||
|
### Account availability
|
||||||
|
|
||||||
|
- Kept a configured Real-Debrid account pool authoritative when every account is disabled instead of reviving legacy browser-login fields.
|
||||||
|
- Released stale in-memory Real-Debrid cooldown and sticky state when an account is explicitly re-enabled so the Start action becomes available without restarting the application.
|
||||||
|
- Preserved configured per-account daily limits and usage while refreshing only transient runtime state.
|
||||||
|
|
||||||
## [2.0.44] - 2026-08-17
|
## [2.0.44] - 2026-08-17
|
||||||
|
|
||||||
### Live updates
|
### Live updates
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-debrid-downloader",
|
"name": "multi-debrid-downloader",
|
||||||
"version": "2.0.44",
|
"version": "2.0.45",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "multi-debrid-downloader",
|
"name": "multi-debrid-downloader",
|
||||||
"version": "2.0.44",
|
"version": "2.0.45",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"adm-zip": "0.6.0",
|
"adm-zip": "0.6.0",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "multi-debrid-downloader",
|
"name": "multi-debrid-downloader",
|
||||||
"version": "2.0.44",
|
"version": "2.0.45",
|
||||||
"description": "Desktop downloader",
|
"description": "Desktop downloader",
|
||||||
"main": "build/main/main/main.js",
|
"main": "build/main/main/main.js",
|
||||||
"author": "Sucukdeluxe",
|
"author": "Sucukdeluxe",
|
||||||
|
|||||||
@@ -714,6 +714,15 @@ export function pruneRealDebridRuntimeStateForAccounts(activeAccountIds: Set<str
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function releaseRealDebridAccountCooldown(accountId: string): boolean {
|
||||||
|
const removed = realDebridAccountCooldowns.delete(accountId);
|
||||||
|
if (realDebridStickyAccountId === accountId) {
|
||||||
|
realDebridStickyAccountId = "";
|
||||||
|
realDebridStickyCount = 0;
|
||||||
|
}
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
|
||||||
export function pruneExpiredRealDebridRuntimeState(now = Date.now()): number {
|
export function pruneExpiredRealDebridRuntimeState(now = Date.now()): number {
|
||||||
let removed = 0;
|
let removed = 0;
|
||||||
for (const [accountId, detail] of realDebridAccountCooldowns) {
|
for (const [accountId, detail] of realDebridAccountCooldowns) {
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ function releaseTlsSkip(): void {
|
|||||||
}
|
}
|
||||||
import { cleanupCancelledPackageArtifactsAsync, removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup";
|
import { cleanupCancelledPackageArtifactsAsync, removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup";
|
||||||
import { planDownloadCompletion, reconcileFinalizedSize, validateDownloadedFileCompletion } from "./download-completion";
|
import { planDownloadCompletion, reconcileFinalizedSize, validateDownloadedFileCompletion } from "./download-completion";
|
||||||
import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkRapidgatorOnline, fetchAllDebridHostInfo, getAvailableDebridLinkApiKeys, getAvailableMegaDebridAccounts, getAvailableRealDebridAccounts, getMegaDebridAccountCooldownState, getMegaDebridInFlightCountForMode, getRealDebridAccountAttemptTimeoutMs, pruneExpiredDebridLinkRuntimeState, pruneExpiredMegaDebridRuntimeState, pruneExpiredRealDebridRuntimeState } from "./debrid";
|
import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkRapidgatorOnline, fetchAllDebridHostInfo, getAvailableDebridLinkApiKeys, getAvailableMegaDebridAccounts, getAvailableRealDebridAccounts, getMegaDebridAccountCooldownState, getMegaDebridInFlightCountForMode, getRealDebridAccountAttemptTimeoutMs, pruneExpiredDebridLinkRuntimeState, pruneExpiredMegaDebridRuntimeState, pruneExpiredRealDebridRuntimeState, releaseRealDebridAccountCooldown } from "./debrid";
|
||||||
import { cleanupArchives, clearExtractResumeState, collectArchiveCleanupTargets, detectArchiveSignature, extractPackageArchives, findArchiveCandidates, hasAnyFilesRecursive, removeEmptyDirectoryTree, resetExtractorCachesForPasswordChange, type ExtractArchiveFailureInfo } from "./extractor";
|
import { cleanupArchives, clearExtractResumeState, collectArchiveCleanupTargets, detectArchiveSignature, extractPackageArchives, findArchiveCandidates, hasAnyFilesRecursive, removeEmptyDirectoryTree, resetExtractorCachesForPasswordChange, type ExtractArchiveFailureInfo } from "./extractor";
|
||||||
import { validateFileAgainstManifest } from "./integrity";
|
import { validateFileAgainstManifest } from "./integrity";
|
||||||
import { classifyDiskError } from "./fs-error";
|
import { classifyDiskError } from "./fs-error";
|
||||||
@@ -2268,6 +2268,15 @@ export class DownloadManager extends EventEmitter {
|
|||||||
|
|
||||||
public setSettings(next: AppSettings, opts?: { suppressRetroactiveCleanup?: boolean; settingsOnlyImport?: boolean }): void {
|
public setSettings(next: AppSettings, opts?: { suppressRetroactiveCleanup?: boolean; settingsOnlyImport?: boolean }): void {
|
||||||
const previous = this.settings;
|
const previous = this.settings;
|
||||||
|
const previousRealDebridAccounts = new Map(getRealDebridAccounts(previous).map((account) => [account.id, account]));
|
||||||
|
const previousRealDebridProviderDisabled = (previous.disabledProviders || []).includes("realdebrid");
|
||||||
|
const nextRealDebridProviderDisabled = (next.disabledProviders || []).includes("realdebrid");
|
||||||
|
const reenabledRealDebridAccountIds = getRealDebridAccounts(next)
|
||||||
|
.filter((account) => account.enabled && (
|
||||||
|
previousRealDebridAccounts.get(account.id)?.enabled === false
|
||||||
|
|| (previousRealDebridProviderDisabled && !nextRealDebridProviderDisabled)
|
||||||
|
))
|
||||||
|
.map((account) => account.id);
|
||||||
const previousMegaPool = (["api", "web"] as const)
|
const previousMegaPool = (["api", "web"] as const)
|
||||||
.flatMap((mode) => getAvailableMegaDebridAccounts(previous, mode).map((account) => `${mode}:${account.id}:${account.password}`))
|
.flatMap((mode) => getAvailableMegaDebridAccounts(previous, mode).map((account) => `${mode}:${account.id}:${account.password}`))
|
||||||
.sort()
|
.sort()
|
||||||
@@ -2300,6 +2309,11 @@ export class DownloadManager extends EventEmitter {
|
|||||||
this.invalidateMegaSessionFn?.();
|
this.invalidateMegaSessionFn?.();
|
||||||
}
|
}
|
||||||
this.debridService.setSettings(next);
|
this.debridService.setSettings(next);
|
||||||
|
for (const accountId of reenabledRealDebridAccountIds) {
|
||||||
|
if (releaseRealDebridAccountCooldown(accountId)) {
|
||||||
|
logger.info(`Settings-Update: Real-Debrid-Cooldown für reaktivierten Account freigegeben (${accountId})`);
|
||||||
|
}
|
||||||
|
}
|
||||||
this.allDebridHostInfoCache.clear();
|
this.allDebridHostInfoCache.clear();
|
||||||
|
|
||||||
const prevOrder = JSON.stringify(previous.providerOrder ?? []);
|
const prevOrder = JSON.stringify(previous.providerOrder ?? []);
|
||||||
@@ -8260,7 +8274,8 @@ export class DownloadManager extends EventEmitter {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (effectiveProvider === "realdebrid") {
|
if (effectiveProvider === "realdebrid") {
|
||||||
return getRealDebridAccounts(this.settings).some((account) => account.enabled)
|
const configuredAccounts = getRealDebridAccounts(this.settings);
|
||||||
|
return configuredAccounts.length > 0
|
||||||
? getAvailableRealDebridAccounts(this.settings).length > 0
|
? getAvailableRealDebridAccounts(this.settings).length > 0
|
||||||
: Boolean(this.settings.realDebridUseWebLogin || this.settings.token.trim());
|
: Boolean(this.settings.realDebridUseWebLogin || this.settings.token.trim());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import { serializeRealDebridApiAccounts } from "../src/shared/real-debrid-accoun
|
|||||||
import { getRenameLogPath, initRenameLog, shutdownRenameLog } from "../src/main/rename-log";
|
import { getRenameLogPath, initRenameLog, shutdownRenameLog } from "../src/main/rename-log";
|
||||||
import { UnrestrictedLink } from "../src/main/realdebrid";
|
import { UnrestrictedLink } from "../src/main/realdebrid";
|
||||||
import { resetVideoToolingCache } from "../src/main/video-processor";
|
import { resetVideoToolingCache } from "../src/main/video-processor";
|
||||||
import type { HistoryEntry, PackageEntry } from "../src/shared/types";
|
import type { AppSettings, HistoryEntry, PackageEntry } from "../src/shared/types";
|
||||||
|
|
||||||
const tempDirs: string[] = [];
|
const tempDirs: string[] = [];
|
||||||
const originalFetch = globalThis.fetch;
|
const originalFetch = globalThis.fetch;
|
||||||
@@ -636,6 +636,59 @@ describe("download start account gate", () => {
|
|||||||
expect(disabledMegaManager.getSnapshot().canStart).toBe(false);
|
expect(disabledMegaManager.getSnapshot().canStart).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not revive the legacy Real-Debrid login when every concrete pool account is disabled", async () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-disabled-pool-gate-"));
|
||||||
|
tempDirs.push(root);
|
||||||
|
const firstId = "rdw_first";
|
||||||
|
const secondId = "rdw_second";
|
||||||
|
const manager = new DownloadManager(
|
||||||
|
{
|
||||||
|
...defaultSettings(),
|
||||||
|
realDebridUseWebLogin: true,
|
||||||
|
realDebridWebAccountIds: [firstId, secondId],
|
||||||
|
realDebridDisabledAccountIds: [firstId, secondId],
|
||||||
|
providerOrder: ["realdebrid"]
|
||||||
|
},
|
||||||
|
emptySession(),
|
||||||
|
createStoragePaths(path.join(root, "state"))
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(manager.getSnapshot().canStart).toBe(false);
|
||||||
|
await expect(manager.start()).rejects.toThrow("Kein aktiver Download-Account verfügbar");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("makes an explicitly re-enabled Real-Debrid account immediately usable after an old cooldown", () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-reenabled-cooldown-gate-"));
|
||||||
|
tempDirs.push(root);
|
||||||
|
const accountId = "rdw_reenabled";
|
||||||
|
const disabledSettings: AppSettings = {
|
||||||
|
...defaultSettings(),
|
||||||
|
realDebridUseWebLogin: true,
|
||||||
|
realDebridWebAccountIds: [accountId],
|
||||||
|
realDebridDisabledAccountIds: [accountId],
|
||||||
|
disabledProviders: ["realdebrid"],
|
||||||
|
providerOrder: ["realdebrid"]
|
||||||
|
};
|
||||||
|
const manager = new DownloadManager(
|
||||||
|
disabledSettings,
|
||||||
|
emptySession(),
|
||||||
|
createStoragePaths(path.join(root, "state"))
|
||||||
|
);
|
||||||
|
primeRealDebridRuntimeCooldownForTests(accountId, 60_000, "Vorheriger Accountfehler");
|
||||||
|
|
||||||
|
expect(manager.getSnapshot().canStart).toBe(false);
|
||||||
|
expect(getProviderRuntimeSnapshot().realDebrid.accounts.find((entry) => entry.accountId === accountId)?.cooldown).not.toBeNull();
|
||||||
|
|
||||||
|
manager.setSettings({
|
||||||
|
...disabledSettings,
|
||||||
|
realDebridDisabledAccountIds: [],
|
||||||
|
disabledProviders: []
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(manager.getSnapshot().canStart).toBe(true);
|
||||||
|
expect(getProviderRuntimeSnapshot().realDebrid.accounts.find((entry) => entry.accountId === accountId)?.cooldown ?? null).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
it("allows start when an active account is available", () => {
|
it("allows start when an active account is available", () => {
|
||||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-active-account-gate-"));
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "rd-active-account-gate-"));
|
||||||
tempDirs.push(root);
|
tempDirs.push(root);
|
||||||
|
|||||||
Reference in New Issue
Block a user