feat(realdebrid): rotate accounts during unrestrict

This commit is contained in:
Sucukdeluxe
2026-08-15 21:16:18 +02:00
parent 53cdac1ded
commit 7e65195057
13 changed files with 1159 additions and 139 deletions
+94 -22
View File
@@ -26,13 +26,16 @@ import { parseDebridLinkApiKeys } from "../shared/debrid-link-keys";
import { extractHosterFromUrl } from "../shared/hoster";
import { isMegaDebridTransientResolveFailure, germanMegaDebridResolveReason } from "../shared/mega-debrid-errors";
import { getMegaDebridAccountsForMode } from "../shared/mega-debrid-accounts";
import {
import { getRealDebridAccounts } from "../shared/real-debrid-accounts";
import {
addDebridLinkApiKeyDailyUsageBytes,
addDebridLinkApiKeyTotalUsageBytes,
addMegaDebridAccountDailyUsageBytes,
addMegaDebridAccountTotalUsageBytes,
addProviderDailyUsageBytes,
addProviderTotalUsageBytes,
addProviderTotalUsageBytes,
addRealDebridAccountDailyUsageBytes,
addRealDebridAccountTotalUsageBytes,
getProviderUsageDayKey,
isProviderDailyLimitReached
} from "../shared/provider-daily-limits";
@@ -55,7 +58,7 @@ function releaseTlsSkip(): void {
}
import { cleanupCancelledPackageArtifactsAsync, removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup";
import { planDownloadCompletion, reconcileFinalizedSize, validateDownloadedFileCompletion } from "./download-completion";
import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkRapidgatorOnline, fetchAllDebridHostInfo, getAvailableDebridLinkApiKeys, getAvailableMegaDebridAccounts, getMegaDebridAccountCooldownState, getMegaDebridInFlightCountForMode, pruneExpiredDebridLinkRuntimeState, pruneExpiredMegaDebridRuntimeState } from "./debrid";
import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkRapidgatorOnline, fetchAllDebridHostInfo, getAvailableDebridLinkApiKeys, getAvailableMegaDebridAccounts, getAvailableRealDebridAccounts, getMegaDebridAccountCooldownState, getMegaDebridInFlightCountForMode, getRealDebridAccountAttemptTimeoutMs, pruneExpiredDebridLinkRuntimeState, pruneExpiredMegaDebridRuntimeState, pruneExpiredRealDebridRuntimeState } from "./debrid";
import { cleanupArchives, clearExtractResumeState, collectArchiveCleanupTargets, detectArchiveSignature, extractPackageArchives, findArchiveCandidates, hasAnyFilesRecursive, removeEmptyDirectoryTree, resetExtractorCachesForPasswordChange, type ExtractArchiveFailureInfo } from "./extractor";
import { validateFileAgainstManifest } from "./integrity";
import { classifyDiskError } from "./fs-error";
@@ -353,13 +356,55 @@ function getPostExtractTimeoutMs(): number {
return DEFAULT_POST_EXTRACT_TIMEOUT_MS;
}
function getUnrestrictTimeoutMs(): number {
function getUnrestrictTimeoutMs(): number {
const fromEnv = Number(process.env.RD_UNRESTRICT_TIMEOUT_MS ?? NaN);
if (Number.isFinite(fromEnv) && fromEnv >= 5000 && fromEnv <= 15 * 60 * 1000) {
return Math.floor(fromEnv);
}
return DEFAULT_UNRESTRICT_TIMEOUT_MS;
}
return DEFAULT_UNRESTRICT_TIMEOUT_MS;
}
export function resolveUnrestrictTimeoutBudgetMs(
baseTimeoutMs: number,
preferredLeadProvider: DebridProvider | null,
settings: AppSettings,
link: string,
realDebridAccountAttemptTimeoutMs = getRealDebridAccountAttemptTimeoutMs()
): number {
const configuredOrder = settings.providerOrder?.length > 0
? [...settings.providerOrder]
: [settings.providerPrimary, settings.providerSecondary, settings.providerTertiary]
.filter((provider): provider is DebridProvider => provider !== "none");
const orderedProviders = preferredLeadProvider
? [preferredLeadProvider, ...configuredOrder.filter((provider) => provider !== preferredLeadProvider)]
: configuredOrder;
const hosterKey = extractHosterFromUrl(link);
const routedProvider = hosterKey ? settings.hosterRouting?.[hosterKey] : undefined;
const routedPlan = routedProvider
? [routedProvider, ...orderedProviders.filter((provider) => provider !== routedProvider)]
: orderedProviders;
const plan = settings.autoProviderFallback || routedProvider ? routedPlan : routedPlan.slice(0, 1);
const uniqueProviders: DebridProvider[] = [];
const seen = new Set<DebridProvider>();
for (const provider of plan) {
const effectiveProvider = resolveMegaDebridProvider(settings, provider) || provider;
if (!seen.has(effectiveProvider)) {
seen.add(effectiveProvider);
uniqueProviders.push(effectiveProvider);
}
}
const accountCount = getAvailableRealDebridAccounts(settings).length;
const budgetMs = uniqueProviders.reduce((total, provider) => {
if (provider !== "realdebrid") {
return total + baseTimeoutMs;
}
if (accountCount === 0) {
return total;
}
return total + Math.max(baseTimeoutMs, realDebridAccountAttemptTimeoutMs * accountCount);
}, 0);
return Math.max(baseTimeoutMs, Math.min(Number.MAX_SAFE_INTEGER, budgetMs));
}
function getLowThroughputTimeoutMs(): number {
const fromEnv = Number(process.env.RD_LOW_THROUGHPUT_TIMEOUT_MS ?? NaN);
@@ -2280,7 +2325,11 @@ export class DownloadManager extends EventEmitter {
}
const credChanges: Array<{ prev: string; next: string; providers: string[] }> = [
{ prev: previous.token || "", next: next.token || "", providers: ["realdebrid"] },
{
prev: `${previous.token || ""}|${previous.realDebridApiTokens || ""}|${(previous.realDebridWebAccountIds || []).join(",")}|${(previous.realDebridDisabledAccountIds || []).join(",")}`,
next: `${next.token || ""}|${next.realDebridApiTokens || ""}|${(next.realDebridWebAccountIds || []).join(",")}|${(next.realDebridDisabledAccountIds || []).join(",")}`,
providers: ["realdebrid"]
},
{ prev: previous.allDebridToken || "", next: next.allDebridToken || "", providers: ["alldebrid"] },
{ prev: previous.bestToken || "", next: next.bestToken || "", providers: ["bestdebrid"] },
{ prev: previous.debridLinkApiKeys || "", next: next.debridLinkApiKeys || "", providers: ["debridlink"] },
@@ -8148,9 +8197,10 @@ export class DownloadManager extends EventEmitter {
return;
}
this.settings.providerDailyUsageDay = currentDay;
this.settings.providerDailyUsageBytes = {};
this.settings.debridLinkApiKeyDailyUsageBytes = {};
this.settings.megaDebridAccountDailyUsageBytes = {};
this.settings.providerDailyUsageBytes = {};
this.settings.debridLinkApiKeyDailyUsageBytes = {};
this.settings.megaDebridAccountDailyUsageBytes = {};
this.settings.realDebridAccountDailyUsageBytes = {};
this.statsCache = null;
this.statsCacheAt = 0;
if (persist) {
@@ -8178,13 +8228,26 @@ export class DownloadManager extends EventEmitter {
this.settings.debridLinkApiKeyDailyUsageBytes = nextKeyUsage.debridLinkApiKeyDailyUsageBytes;
this.settings.debridLinkApiKeyTotalUsageBytes = nextKeyTotalUsage.debridLinkApiKeyTotalUsageBytes;
}
if ((effectiveProvider === "megadebrid-api" || effectiveProvider === "megadebrid-web") && providerAccountId) {
if ((effectiveProvider === "megadebrid-api" || effectiveProvider === "megadebrid-web") && providerAccountId) {
const nextAcctUsage = addMegaDebridAccountDailyUsageBytes(this.settings, providerAccountId, byteDelta);
const nextAcctTotalUsage = addMegaDebridAccountTotalUsageBytes(this.settings, providerAccountId, byteDelta);
this.settings.providerDailyUsageDay = nextAcctUsage.providerDailyUsageDay;
this.settings.megaDebridAccountDailyUsageBytes = nextAcctUsage.megaDebridAccountDailyUsageBytes;
this.settings.megaDebridAccountTotalUsageBytes = nextAcctTotalUsage.megaDebridAccountTotalUsageBytes;
}
this.settings.megaDebridAccountTotalUsageBytes = nextAcctTotalUsage.megaDebridAccountTotalUsageBytes;
}
const realDebridAccounts = effectiveProvider === "realdebrid" ? getRealDebridAccounts(this.settings) : [];
const realDebridAccountStillConfigured = Boolean(providerAccountId) && (
realDebridAccounts.some((account) => account.id === providerAccountId)
|| (realDebridAccounts.length === 0 && providerAccountId === "rda_legacy_1" && Boolean(this.settings.token.trim()))
|| (realDebridAccounts.length === 0 && providerAccountId === "rdw_legacy" && this.settings.realDebridUseWebLogin)
);
if (effectiveProvider === "realdebrid" && providerAccountId && realDebridAccountStillConfigured) {
const nextAcctUsage = addRealDebridAccountDailyUsageBytes(this.settings, providerAccountId, byteDelta);
const nextAcctTotalUsage = addRealDebridAccountTotalUsageBytes(this.settings, providerAccountId, byteDelta);
this.settings.providerDailyUsageDay = nextAcctUsage.providerDailyUsageDay;
this.settings.realDebridAccountDailyUsageBytes = nextAcctUsage.realDebridAccountDailyUsageBytes;
this.settings.realDebridAccountTotalUsageBytes = nextAcctTotalUsage.realDebridAccountTotalUsageBytes;
}
}
private isProviderConfigured(provider: DebridProvider): boolean {
@@ -8196,8 +8259,10 @@ export class DownloadManager extends EventEmitter {
if (isProviderDailyLimitReached(this.settings, effectiveProvider)) {
return false;
}
if (effectiveProvider === "realdebrid") {
return Boolean(this.settings.realDebridUseWebLogin || this.settings.token.trim());
if (effectiveProvider === "realdebrid") {
return getRealDebridAccounts(this.settings).some((account) => account.enabled)
? getAvailableRealDebridAccounts(this.settings).length > 0
: Boolean(this.settings.realDebridUseWebLogin || this.settings.token.trim());
}
if (effectiveProvider === "megadebrid-api") {
const hasMegaCreds = getAvailableMegaDebridAccounts(this.settings, "api").length > 0;
@@ -8646,10 +8711,11 @@ export class DownloadManager extends EventEmitter {
allDebridPruned += 1;
}
}
const dlPruned = pruneExpiredDebridLinkRuntimeState(now);
const mdPruned = pruneExpiredMegaDebridRuntimeState(now);
if (allDebridPruned > 0 || dlPruned > 0 || mdPruned > 0) {
logger.info(`Soft-Reset: pruned ${allDebridPruned} AllDebrid host entries, ${dlPruned} Debrid-Link entries, ${mdPruned} Mega-Debrid entries`);
const dlPruned = pruneExpiredDebridLinkRuntimeState(now);
const mdPruned = pruneExpiredMegaDebridRuntimeState(now);
const rdPruned = pruneExpiredRealDebridRuntimeState(now);
if (allDebridPruned > 0 || dlPruned > 0 || mdPruned > 0 || rdPruned > 0) {
logger.info(`Soft-Reset: pruned ${allDebridPruned} AllDebrid host entries, ${dlPruned} Debrid-Link entries, ${mdPruned} Mega-Debrid entries, ${rdPruned} Real-Debrid entries`);
}
}
@@ -9249,7 +9315,13 @@ export class DownloadManager extends EventEmitter {
this.emitState();
return;
}
const unrestrictTimeoutSignal = AbortSignal.timeout(getUnrestrictTimeoutMs());
const unrestrictTimeoutMs = resolveUnrestrictTimeoutBudgetMs(
getUnrestrictTimeoutMs(),
preferredLeadProvider,
this.settings,
item.url
);
const unrestrictTimeoutSignal = AbortSignal.timeout(unrestrictTimeoutMs);
const unrestrictedSignal = AbortSignal.any([active.abortController.signal, unrestrictTimeoutSignal]);
let unrestricted;
try {
@@ -9270,7 +9342,7 @@ export class DownloadManager extends EventEmitter {
traceConversionPhase({
phase: "caller-timeout",
outcome: "timeout",
detail: `Caller-Budget ${Math.ceil(getUnrestrictTimeoutMs() / 1000)}s erschoepft (siehe letzte Phase fuer in-flight Provider/Account)`
detail: `Caller-Budget ${Math.ceil(unrestrictTimeoutMs / 1000)}s erschoepft (siehe letzte Phase fuer in-flight Provider/Account)`
});
}
throw innerError;
@@ -9280,7 +9352,7 @@ export class DownloadManager extends EventEmitter {
} catch (unrestrictError) {
if (!active.abortController.signal.aborted && unrestrictTimeoutSignal.aborted) {
this.recordProviderFailure(cooldownProvider);
throw new Error(`Unrestrict Timeout nach ${Math.ceil(getUnrestrictTimeoutMs() / 1000)}s`);
throw new Error(`Unrestrict Timeout nach ${Math.ceil(unrestrictTimeoutMs / 1000)}s`);
}
const errText = compactErrorText(unrestrictError);
if (isUnrestrictFailure(errText) && !isHosterUnavailableError(errText)) {