import { error as logError, warn } from "../lib/log"; import { normalizeLanguage } from "./jellyfin"; export interface SonarrConfig { url: string; apiKey: string; } function headers(apiKey: string): Record { return { "X-Api-Key": apiKey }; } /** True only when url is an http(s) URL and apiKey is non-empty. */ export function isUsable(cfg: SonarrConfig): boolean { if (!cfg.apiKey) return false; try { const u = new URL(cfg.url); return u.protocol === "http:" || u.protocol === "https:"; } catch { return false; } } export async function testConnection(cfg: SonarrConfig): Promise<{ ok: boolean; error?: string }> { if (!isUsable(cfg)) return { ok: false, error: "Sonarr URL/API key not configured" }; try { const res = await fetch(`${cfg.url}/api/v3/system/status`, { headers: headers(cfg.apiKey), }); if (!res.ok) return { ok: false, error: `HTTP ${res.status}` }; return { ok: true }; } catch (e) { return { ok: false, error: String(e) }; } } interface SonarrSeries { tvdbId?: number; originalLanguage?: { name: string }; } /** Pre-loaded Sonarr library indexed for O(1) per-series lookups during a scan. */ export interface SonarrLibrary { byTvdbId: Map; } async function fetchJson(url: string, cfg: SonarrConfig, context: string): Promise { try { const res = await fetch(url, { headers: headers(cfg.apiKey) }); if (!res.ok) { warn(`Sonarr ${context} → HTTP ${res.status}`); return null; } return (await res.json()) as T; } catch (e) { logError(`Sonarr ${context} failed:`, (e as Error).message); return null; } } export async function loadLibrary(cfg: SonarrConfig): Promise { const empty: SonarrLibrary = { byTvdbId: new Map() }; if (!isUsable(cfg)) return empty; const list = await fetchJson(`${cfg.url}/api/v3/series`, cfg, "library/all"); if (!list) return empty; const byTvdbId = new Map(); for (const s of list) { if (s.tvdbId != null) byTvdbId.set(String(s.tvdbId), s); } return { byTvdbId }; } /** Returns ISO 639-2 original language for a series or null. */ export async function getOriginalLanguage( cfg: SonarrConfig, tvdbId: string, library: SonarrLibrary, ): Promise { const m = library.byTvdbId.get(tvdbId); if (m?.originalLanguage) return nameToIso(m.originalLanguage.name); if (!isUsable(cfg)) return null; const lookup = await fetchJson( `${cfg.url}/api/v3/series/lookup?term=tvdb%3A${tvdbId}`, cfg, "lookup/tvdb", ); const fromLookup = lookup?.find((s) => String(s.tvdbId ?? "") === String(tvdbId)) ?? lookup?.[0]; if (fromLookup?.originalLanguage) return nameToIso(fromLookup.originalLanguage.name); return null; } const NAME_TO_639_2: Record = { english: "eng", french: "fra", german: "deu", spanish: "spa", italian: "ita", portuguese: "por", japanese: "jpn", korean: "kor", chinese: "zho", mandarin: "zho", cantonese: "zho", arabic: "ara", russian: "rus", dutch: "nld", flemish: "nld", swedish: "swe", norwegian: "nor", danish: "dan", finnish: "fin", polish: "pol", turkish: "tur", thai: "tha", hindi: "hin", hungarian: "hun", czech: "ces", romanian: "ron", greek: "ell", hebrew: "heb", persian: "fas", farsi: "fas", ukrainian: "ukr", indonesian: "ind", malay: "msa", vietnamese: "vie", catalan: "cat", tamil: "tam", telugu: "tel", icelandic: "isl", croatian: "hrv", bulgarian: "bul", serbian: "srp", slovak: "slk", slovenian: "slv", latvian: "lav", lithuanian: "lit", estonian: "est", }; function nameToIso(name: string): string | null { const key = name.toLowerCase().trim(); if (NAME_TO_639_2[key]) return NAME_TO_639_2[key]; if (key.length === 2 || key.length === 3) return normalizeLanguage(key); warn(`Sonarr language name not recognised: '${name}'.`); return null; }