mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-04 21:18:31 +01:00
The resolver field only needs to find the working URL — the Jellyseerr version requirement is irrelevant there and only polluted the UI. - jellyseerrProbe: validate reachability + that it's a jellyseerr (no version gate, no version-too-low outcome). - Drop the version-too-low reason from the whole resolver stack (types, resolve, hook, status text, i18n). - Min version 2.0.0 stays enforced in JellyseerrApi.test() at login: now writes an error log + toast, and uses numeric isVersionBelow (fixes the "2.10.0" < "2.0.0" string-compare bug).
89 lines
2.6 KiB
TypeScript
89 lines
2.6 KiB
TypeScript
import { getServerUrlCandidates } from "./candidates";
|
|
import type { ServerProbe, ServerProbeOutcome } from "./types";
|
|
|
|
export type ResolveFailureReason =
|
|
| "empty"
|
|
| "invalid"
|
|
| "wrong-service"
|
|
| "unreachable";
|
|
|
|
export type ResolveResult =
|
|
| { ok: true; url: string; meta?: Record<string, unknown> }
|
|
| { ok: false; reason: ResolveFailureReason };
|
|
|
|
export interface ResolveOptions {
|
|
/** Per-candidate probe timeout in ms. Default 5000. */
|
|
timeoutMs?: number;
|
|
/** Abort the whole resolution (cancels every in-flight probe). */
|
|
signal?: AbortSignal;
|
|
}
|
|
|
|
// Order in which to surface a failure when no candidate validated:
|
|
// the more specific/actionable the reason, the earlier it is reported.
|
|
const FAILURE_PRIORITY = [
|
|
"wrong-service",
|
|
"unreachable",
|
|
] as const satisfies ReadonlyArray<ResolveFailureReason>;
|
|
|
|
/**
|
|
* Resolve loose user input to a single working, canonical server URL.
|
|
*
|
|
* Generates candidates (https-first), probes them in parallel with a per-candidate
|
|
* timeout, and returns the first candidate (in preference order) the probe
|
|
* accepted. When none work, the most actionable failure is returned.
|
|
*/
|
|
export async function resolveServerUrl(
|
|
input: string,
|
|
probe: ServerProbe,
|
|
options: ResolveOptions = {},
|
|
): Promise<ResolveResult> {
|
|
const { timeoutMs = 5000, signal } = options;
|
|
|
|
if (!input.trim()) return { ok: false, reason: "empty" };
|
|
|
|
const candidates = getServerUrlCandidates(input);
|
|
if (candidates.length === 0) return { ok: false, reason: "invalid" };
|
|
|
|
const outcomes = await Promise.all(
|
|
candidates.map((url) => runProbe(url, probe, timeoutMs, signal)),
|
|
);
|
|
|
|
// Prefer the first candidate (https-first) that validated.
|
|
for (let i = 0; i < candidates.length; i++) {
|
|
const outcome = outcomes[i];
|
|
if (outcome.status === "ok") {
|
|
return { ok: true, url: candidates[i], meta: outcome.meta };
|
|
}
|
|
}
|
|
|
|
// Nothing validated: report the most useful failure.
|
|
for (const reason of FAILURE_PRIORITY) {
|
|
const hit = outcomes.find((outcome) => outcome.status === reason);
|
|
if (hit) {
|
|
return { ok: false, reason };
|
|
}
|
|
}
|
|
return { ok: false, reason: "unreachable" };
|
|
}
|
|
|
|
async function runProbe(
|
|
url: string,
|
|
probe: ServerProbe,
|
|
timeoutMs: number,
|
|
parentSignal?: AbortSignal,
|
|
): Promise<ServerProbeOutcome> {
|
|
const controller = new AbortController();
|
|
const abort = () => controller.abort();
|
|
parentSignal?.addEventListener("abort", abort);
|
|
const timer = setTimeout(abort, timeoutMs);
|
|
|
|
try {
|
|
return await probe(url, controller.signal);
|
|
} catch {
|
|
return { status: "unreachable" };
|
|
} finally {
|
|
clearTimeout(timer);
|
|
parentSignal?.removeEventListener("abort", abort);
|
|
}
|
|
}
|