mirror of
https://github.com/streamyfin/streamyfin.git
synced 2026-06-05 13:38:27 +01:00
Type a loose address (media.example.com, https://…, host:port) and the app finds the working, canonical URL. - utils/serverUrl: generic candidate generator (https-first, port/path preserved, no Jellyfin-specific ports), parallel-probe resolver, numeric semver compare, and a Jellyseerr probe (/api/v1/status, min 2.0.0). - useServerUrlResolver: idle -> resolving -> ok | error state machine with cancellation. - ServerUrlField: shared input that auto-resolves on blur, inline status chip (tap to re-test) + resolved URL, persists the canonical URL. - Jellyseerr settings adopt the field and log in with the resolved URL. Probe contract makes Streamystats/Jellyfin/Merlin a drop-in follow-up.
76 lines
2.3 KiB
TypeScript
76 lines
2.3 KiB
TypeScript
/**
|
|
* Generic server-URL candidate generator.
|
|
*
|
|
* Turns loose user input (`media.uruk.dev`, `https://media.uruk.dev`,
|
|
* `host:8096`, `http://10.0.0.5:3000/path`) into an ordered list of full URLs
|
|
* to probe — https first, http as fallback — while preserving any explicit
|
|
* port and path. Service-agnostic: unlike the Jellyfin SDK's `getAddressCandidates`
|
|
* it adds no Jellyfin-specific ports, so it suits Jellyseerr/Streamystats/etc.
|
|
*/
|
|
|
|
// scheme? host (port)? (path/query/hash)?
|
|
const URL_RE = /^(?:(https?):\/\/)?([^/:\s?#]+)(?::(\d+))?([/?#].*)?$/i;
|
|
|
|
export interface ParsedServerInput {
|
|
scheme?: "http" | "https";
|
|
host: string;
|
|
port?: string;
|
|
/** Normalized path+query+hash, without a trailing slash; "" when none. */
|
|
path: string;
|
|
}
|
|
|
|
function normalizePath(path?: string): string {
|
|
if (!path || path === "/") return "";
|
|
return path.replace(/\/+$/, "");
|
|
}
|
|
|
|
/** Parse loose user input. Returns null when it can't be understood. */
|
|
export function parseServerInput(input: string): ParsedServerInput | null {
|
|
const trimmed = input.trim();
|
|
if (!trimmed) return null;
|
|
|
|
const match = URL_RE.exec(trimmed);
|
|
if (!match) return null;
|
|
|
|
const [, scheme, host, port, rawPath] = match;
|
|
return {
|
|
scheme: scheme ? (scheme.toLowerCase() as "http" | "https") : undefined,
|
|
host: host.toLowerCase(),
|
|
port,
|
|
path: normalizePath(rawPath),
|
|
};
|
|
}
|
|
|
|
function buildUrl(
|
|
scheme: "http" | "https",
|
|
host: string,
|
|
port: string | undefined,
|
|
path: string,
|
|
): string {
|
|
return `${scheme}://${host}${port ? `:${port}` : ""}${path}`;
|
|
}
|
|
|
|
/**
|
|
* Ordered, de-duplicated candidate URLs for the given input.
|
|
*
|
|
* - Explicit scheme AND port → trusted as-is (single candidate).
|
|
* - Otherwise https is tried before http (prefer secure), keeping any port/path.
|
|
*
|
|
* @returns [] when the input can't be parsed.
|
|
*/
|
|
export function getServerUrlCandidates(input: string): string[] {
|
|
const parsed = parseServerInput(input);
|
|
if (!parsed) return [];
|
|
|
|
const { scheme, host, port, path } = parsed;
|
|
|
|
// Fully specified: don't second-guess the user.
|
|
if (scheme && port) return [buildUrl(scheme, host, port, path)];
|
|
|
|
// Secure-first; the typed scheme (if any) is still covered by this set.
|
|
const candidates = (["https", "http"] as const).map((s) =>
|
|
buildUrl(s, host, port, path),
|
|
);
|
|
return Array.from(new Set(candidates));
|
|
}
|