Align provider selection and abort observation

Share one pure provider selection plan between real unrestrict routing and cooldown projection, including direct 1Fichier and DDownload paths, disabled Mega aliases, and the secondary-provider exception used when Real-Debrid is cooling down. Attach terminal observers to raw serialized web jobs before evaluating already-aborted signals so late rejections stay handled while the queue remains available. Add RED-to-GREEN regressions for each provider-selection counterexample and pre-aborted Real-Debrid, AllDebrid, and BestDebrid jobs.
This commit is contained in:
Sucukdeluxe
2026-08-22 11:05:42 +02:00
parent ab31d04410
commit f57b513625
10 changed files with 336 additions and 77 deletions
+3 -3
View File
@@ -75,9 +75,6 @@ async function raceWithAbort<T>(promise: Promise<T>, signal?: AbortSignal): Prom
if (!signal) {
return promise;
}
if (signal.aborted) {
throw abortError();
}
return new Promise<T>((resolve, reject) => {
let settled = false;
const onAbort = (): void => {
@@ -104,6 +101,9 @@ async function raceWithAbort<T>(promise: Promise<T>, signal?: AbortSignal): Prom
signal.removeEventListener("abort", onAbort);
reject(error);
});
if (signal.aborted) {
onAbort();
}
});
}
+3 -3
View File
@@ -33,9 +33,6 @@ async function raceWithAbort<T>(promise: Promise<T>, signal?: AbortSignal): Prom
if (!signal) {
return promise;
}
if (signal.aborted) {
throw abortError();
}
return new Promise<T>((resolve, reject) => {
let settled = false;
const onAbort = (): void => {
@@ -62,6 +59,9 @@ async function raceWithAbort<T>(promise: Promise<T>, signal?: AbortSignal): Prom
signal.removeEventListener("abort", onAbort);
reject(error);
});
if (signal.aborted) {
onAbort();
}
});
}
+119 -66
View File
@@ -1636,14 +1636,50 @@ function toProviderOrder(primary: DebridProvider, secondary: DebridFallbackProvi
return uniqueProviderOrder(order);
}
export function leadProviderChainWith(order: readonly DebridProvider[], preferred: DebridProvider | null | undefined): DebridProvider[] {
export function leadProviderChainWith(order: readonly DebridProvider[], preferred: DebridProvider | null | undefined): DebridProvider[] {
if (!preferred || !order.includes(preferred)) {
return [...order];
}
return [preferred, ...order.filter((provider) => provider !== preferred)];
}
function isRapidgatorLink(link: string): boolean {
return [preferred, ...order.filter((provider) => provider !== preferred)];
}
type ProviderSelectionPlan = {
hosterKey: string;
routedProvider?: DebridProvider;
directProviders: DebridProvider[];
order: DebridProvider[];
};
function buildProviderSelectionPlan(
settings: AppSettings,
link: string,
preferredLeadProvider?: DebridProvider | null
): ProviderSelectionPlan {
const configuredOrder = settings.providerOrder && settings.providerOrder.length > 0
? uniqueProviderOrder(settings.providerOrder)
: toProviderOrder(settings.providerPrimary, settings.providerSecondary, settings.providerTertiary);
const order = leadProviderChainWith(configuredOrder, preferredLeadProvider);
const hosterKey = extractHosterFromUrl(link);
const routedProvider = hosterKey ? settings.hosterRouting?.[hosterKey] : undefined;
const directProviders: DebridProvider[] = [];
if (isOneFichierLink(link)) {
directProviders.push("onefichier");
}
if (DDOWNLOAD_URL_RE.test(link)) {
directProviders.push("ddownload");
}
return { hosterKey, routedProvider, directProviders, order };
}
export function isProviderDisabledForSelection(settings: AppSettings, provider: DebridProvider): boolean {
const effectiveProvider = resolveMegaDebridProvider(settings, provider);
const disabled = settings.disabledProviders || [];
return disabled.includes(provider)
|| disabled.includes(effectiveProvider)
|| ((effectiveProvider === "megadebrid-api" || effectiveProvider === "megadebrid-web") && disabled.includes("megadebrid"));
}
function isRapidgatorLink(link: string): boolean {
try {
const hostname = new URL(link).hostname.toLowerCase();
return hostname === "rapidgator.net"
@@ -4151,25 +4187,62 @@ export class DebridService {
public getBlockingProviderRetryAt(link: string, preferredLeadProvider: DebridProvider | null = null, now = Date.now()): number | null {
const settings = cloneSettings(this.settings);
const configuredOrder = settings.providerOrder && settings.providerOrder.length > 0
? uniqueProviderOrder(settings.providerOrder)
: toProviderOrder(settings.providerPrimary, settings.providerSecondary, settings.providerTertiary);
const orderedProviders = leadProviderChainWith(configuredOrder, preferredLeadProvider);
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 plan = buildProviderSelectionPlan(settings, link, preferredLeadProvider);
const deadlines: number[] = [];
if (plan.routedProvider) {
const routedState = this.getProviderRuntimeWaitState(settings, plan.routedProvider, plan.hosterKey, now);
if (routedState.configured) {
if (routedState.retryAt === null) {
return null;
}
deadlines.push(routedState.retryAt);
}
}
for (const provider of plan.directProviders) {
const state = this.getProviderRuntimeWaitState(settings, provider, plan.hosterKey, now);
if (state.configured && state.retryAt === null) {
return null;
}
if (state.retryAt) {
deadlines.push(state.retryAt);
}
}
if (!settings.autoProviderFallback) {
const primary = plan.order[0];
if (!primary || !this.isProviderConfiguredFor(settings, primary)) {
return deadlines.length > 0 ? Math.min(...deadlines) : null;
}
if (this.isProviderDailyLimited(settings, primary)) {
const secondary = plan.order.find((provider) => provider !== primary && this.isProviderSelectableFor(settings, provider));
if (secondary) {
const secondaryState = this.getProviderRuntimeWaitState(settings, secondary, plan.hosterKey, now);
if (secondaryState.retryAt === null) {
return null;
}
deadlines.push(secondaryState.retryAt);
} else {
const primaryState = this.getProviderRuntimeWaitState(settings, primary, plan.hosterKey, now);
if (primaryState.retryAt) {
deadlines.push(primaryState.retryAt);
}
}
} else {
const primaryState = this.getProviderRuntimeWaitState(settings, primary, plan.hosterKey, now);
if (primaryState.retryAt === null) {
return null;
}
deadlines.push(primaryState.retryAt);
}
return deadlines.length > 0 ? Math.min(...deadlines) : null;
}
const seen = new Set<DebridProvider>();
for (const provider of plan) {
for (const provider of plan.order) {
const effectiveProvider = resolveMegaDebridProvider(settings, provider);
if (seen.has(effectiveProvider)) {
continue;
}
seen.add(effectiveProvider);
const state = this.getProviderRuntimeWaitState(settings, effectiveProvider, hosterKey, now);
const state = this.getProviderRuntimeWaitState(settings, provider, plan.hosterKey, now);
if (!state.configured) {
continue;
}
@@ -4187,10 +4260,11 @@ export class DebridService {
hosterKey: string,
now: number
): { configured: boolean; retryAt: number | null } {
if ((settings.disabledProviders || []).includes(provider)) {
if (isProviderDisabledForSelection(settings, provider)) {
return { configured: false, retryAt: null };
}
if (provider === "realdebrid") {
const effectiveProvider = resolveMegaDebridProvider(settings, provider);
if (effectiveProvider === "realdebrid") {
const accounts = this.getConfiguredRealDebridAccounts(settings).filter((account) => account.enabled
&& !isRealDebridAccountDailyLimitReached(settings, account.id, now));
if (accounts.length === 0) {
@@ -4201,8 +4275,8 @@ export class DebridService {
? { configured: true, retryAt: null }
: { configured: true, retryAt: Math.min(...deadlines as number[]) };
}
if (provider === "megadebrid-api" || provider === "megadebrid-web") {
const mode = provider === "megadebrid-web" ? "web" : "api";
if (effectiveProvider === "megadebrid-api" || effectiveProvider === "megadebrid-web") {
const mode = effectiveProvider === "megadebrid-web" ? "web" : "api";
if (!isMegaDebridModeEnabled(settings, mode) || (mode === "web" && !this.options.megaWebUnrestrict)) {
return { configured: false, retryAt: null };
}
@@ -4216,7 +4290,7 @@ export class DebridService {
? { configured: true, retryAt: null }
: { configured: true, retryAt: Math.min(...deadlines as number[]) };
}
if (provider === "debridlink") {
if (effectiveProvider === "debridlink") {
const keys = parseDebridLinkApiKeys(settings.debridLinkApiKeys).filter((key) => !isDebridLinkApiKeyDisabled(settings, key.id)
&& !isDebridLinkApiKeyDailyLimitReached(settings, key.id, now));
if (keys.length === 0) {
@@ -4398,13 +4472,11 @@ export class DebridService {
return `${PROVIDER_LABELS[effectiveProvider]} Tageslimit erreicht`;
}
public async unrestrictLink(link: string, signal?: AbortSignal, settingsSnapshot?: AppSettings, preferredLeadProvider?: DebridProvider | null): Promise<ProviderUnrestrictedLink> {
const settings = settingsSnapshot ? cloneSettings(settingsSnapshot) : cloneSettings(this.settings);
const routing = settings.hosterRouting || {};
const hosterKey = extractHosterFromUrl(link);
if (hosterKey && routing[hosterKey]) {
const routedProvider = routing[hosterKey];
public async unrestrictLink(link: string, signal?: AbortSignal, settingsSnapshot?: AppSettings, preferredLeadProvider?: DebridProvider | null): Promise<ProviderUnrestrictedLink> {
const settings = settingsSnapshot ? cloneSettings(settingsSnapshot) : cloneSettings(this.settings);
const selectionPlan = buildProviderSelectionPlan(settings, link, preferredLeadProvider);
const { hosterKey, routedProvider } = selectionPlan;
if (hosterKey && routedProvider) {
if (this.isProviderSelectableFor(settings, routedProvider)) {
logger.info(`Hoster-Zuordnung: ${hosterKey}${PROVIDER_LABELS[routedProvider]}`);
try {
@@ -4437,14 +4509,17 @@ export class DebridService {
}
}
if (isOneFichierLink(link) && this.isProviderSelectableFor(settings, "onefichier")) {
try {
const result = await this.unrestrictViaProvider(settings, "onefichier", link, signal);
return {
...result,
provider: "onefichier",
providerLabel: PROVIDER_LABELS["onefichier"] + (result.sourceLabel ? ` (${result.sourceLabel})` : "")
};
for (const directProvider of selectionPlan.directProviders) {
if (!this.isProviderSelectableFor(settings, directProvider)) {
continue;
}
try {
const result = await this.unrestrictViaProvider(settings, directProvider, link, signal);
return {
...result,
provider: directProvider,
providerLabel: PROVIDER_LABELS[directProvider] + (result.sourceLabel ? ` (${result.sourceLabel})` : "")
};
} catch (error) {
const errorText = compactErrorText(error);
if (signal?.aborted || (/aborted/i.test(errorText) && !/timeout/i.test(errorText))) {
@@ -4453,32 +4528,10 @@ export class DebridService {
if (!settings.autoProviderFallback) {
throw error;
}
}
}
if (DDOWNLOAD_URL_RE.test(link) && this.isProviderSelectableFor(settings, "ddownload")) {
try {
const result = await this.unrestrictViaProvider(settings, "ddownload", link, signal);
return {
...result,
provider: "ddownload",
providerLabel: PROVIDER_LABELS["ddownload"] + (result.sourceLabel ? ` (${result.sourceLabel})` : "")
};
} catch (error) {
const errorText = compactErrorText(error);
if (signal?.aborted || (/aborted/i.test(errorText) && !/timeout/i.test(errorText))) {
throw error;
}
if (!settings.autoProviderFallback) {
throw error;
}
}
}
const baseOrder: DebridProvider[] = (settings.providerOrder && settings.providerOrder.length > 0)
? uniqueProviderOrder(settings.providerOrder)
: toProviderOrder(settings.providerPrimary, settings.providerSecondary, settings.providerTertiary);
const order = leadProviderChainWith(baseOrder, preferredLeadProvider);
}
}
const order = selectionPlan.order;
const primary = order[0];
if (!settings.autoProviderFallback) {
@@ -4584,9 +4637,9 @@ export class DebridService {
throw new Error(`Unrestrict fehlgeschlagen: ${attempts.join(" | ")}`);
}
private isProviderConfiguredFor(settings: AppSettings, provider: DebridProvider): boolean {
const effectiveProvider = resolveMegaDebridProvider(settings, provider);
if ((settings.disabledProviders || []).includes(provider) || (settings.disabledProviders || []).includes(effectiveProvider)) return false;
private isProviderConfiguredFor(settings: AppSettings, provider: DebridProvider): boolean {
const effectiveProvider = resolveMegaDebridProvider(settings, provider);
if (isProviderDisabledForSelection(settings, provider)) return false;
if (effectiveProvider === "realdebrid") {
return this.getConfiguredRealDebridAccounts(settings).some((account) => account.enabled);
}
+2 -2
View File
@@ -62,7 +62,7 @@ function releaseTlsSkip(): void {
}
import { cleanupCancelledPackageArtifactsAsync, removeDownloadLinkArtifacts, removeSampleArtifacts } from "./cleanup";
import { planDownloadCompletion, reconcileFinalizedSize, validateDownloadedFileCompletion } from "./download-completion";
import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkDdownloadOnline, checkOneFichierLinks, checkRapidgatorOnline, fetchAllDebridHostInfo, filenameFromDdownloadUrlPath, getAvailableDebridLinkApiKeys, getAvailableMegaDebridAccounts, getAvailableRealDebridAccounts, getMegaDebridAccountCooldownState, getMegaDebridInFlightCountForMode, getRealDebridAccountAttemptTimeoutMs, isDdownloadLink, isOneFichierLink, pruneExpiredDebridLinkRuntimeState, pruneExpiredMegaDebridRuntimeState, pruneExpiredRealDebridRuntimeState, releaseRealDebridAccountCooldown, type DdownloadCheckResult, type OneFichierCheckResult } from "./debrid";
import { AllDebridWebUnrestrictor, BestDebridWebUnrestrictor, DebridService, MegaWebUnrestrictor, RealDebridWebUnrestrictor, checkDdownloadOnline, checkOneFichierLinks, checkRapidgatorOnline, fetchAllDebridHostInfo, filenameFromDdownloadUrlPath, getAvailableDebridLinkApiKeys, getAvailableMegaDebridAccounts, getAvailableRealDebridAccounts, getMegaDebridAccountCooldownState, getMegaDebridInFlightCountForMode, getRealDebridAccountAttemptTimeoutMs, isDdownloadLink, isOneFichierLink, isProviderDisabledForSelection, pruneExpiredDebridLinkRuntimeState, pruneExpiredMegaDebridRuntimeState, pruneExpiredRealDebridRuntimeState, releaseRealDebridAccountCooldown, type DdownloadCheckResult, type OneFichierCheckResult } from "./debrid";
import { cleanupArchives, clearExtractResumeState, collectArchiveCleanupTargets, detectArchiveSignature, extractPackageArchives, findArchiveCandidates, hasAnyFilesRecursive, removeEmptyDirectoryTree, resetExtractorCachesForPasswordChange, type ExtractArchiveFailureInfo, type ExtractProgressUpdate } from "./extractor";
import { validateFileAgainstManifest } from "./integrity";
import { classifyDiskError } from "./fs-error";
@@ -9017,7 +9017,7 @@ export class DownloadManager extends EventEmitter {
private isProviderConfigured(provider: DebridProvider): boolean {
this.ensureProviderDailyUsageFresh(nowMs());
const effectiveProvider = resolveMegaDebridProvider(this.settings, provider) || provider;
if ((this.settings.disabledProviders || []).includes(provider) || (this.settings.disabledProviders || []).includes(effectiveProvider)) {
if (isProviderDisabledForSelection(this.settings, provider)) {
return false;
}
if (isProviderDailyLimitReached(this.settings, effectiveProvider)) {
+3 -3
View File
@@ -80,9 +80,6 @@ async function raceWithAbort<T>(promise: Promise<T>, signal?: AbortSignal): Prom
if (!signal) {
return promise;
}
if (signal.aborted) {
throw abortError();
}
return new Promise<T>((resolve, reject) => {
let settled = false;
const onAbort = (): void => {
@@ -109,6 +106,9 @@ async function raceWithAbort<T>(promise: Promise<T>, signal?: AbortSignal): Prom
signal.removeEventListener("abort", onAbort);
reject(error);
});
if (signal.aborted) {
onAbort();
}
});
}