// Shared HTTP helpers. The TV app talks to Radarr, Sonarr, SABnzbd and Trakt // directly from the browser: all four answer cross-origin requests (CORS). export class HTTPError extends Error { status: number; constructor(status: number, message: string) { super(message ? `HTTP ${status}: ${message}` : `unexpected response: HTTP ${status}`); this.status = status; } } export class NotConfiguredError extends Error { constructor(what: string) { super(`${what} is not configured: set its address and API key in Settings`); } } // request sends a request that gives up after timeoutMs. Network failures // become an Error naming the host only (the URL may carry an API key). export async function request(url: string, init: RequestInit = {}, timeoutMs = 30_000): Promise { const ctl = new AbortController(); const timer = setTimeout(() => ctl.abort(), timeoutMs); const outer = init.signal; if (outer) { if (outer.aborted) ctl.abort(); else outer.addEventListener("abort", () => ctl.abort()); } try { return await fetch(url, { ...init, signal: ctl.signal }); } catch (e) { if (outer?.aborted) throw new DOMException("cancelled", "AbortError"); if (ctl.signal.aborted) throw new Error(`${hostOf(url)}: no answer within ${Math.round(timeoutMs / 1000)} s`); // A browser cannot follow a redirect (e.g. http to https) of a cross-origin // request unless the redirect itself allows it; the TUI never notices. const hint = url.startsWith("http://") ? "; if the address redirects to https, enter the https address" : ""; throw new Error(`${hostOf(url)}: cannot connect (${(e as Error).message})${hint}`); } finally { clearTimeout(timer); } } export function hostOf(url: string): string { try { return new URL(url).host; } catch { return "server"; } } export function isAbort(e: unknown): boolean { return e instanceof DOMException && e.name === "AbortError"; } export function query(params: Record): string { const parts: string[] = []; for (const [k, v] of Object.entries(params)) { if (v !== undefined) parts.push(encodeURIComponent(k) + "=" + encodeURIComponent(String(v))); } return parts.length ? "?" + parts.join("&") : ""; } export function trimSlash(u: string): string { return u.trim().replace(/\/+$/, ""); }