import type { Database } from "bun:sqlite"; import type { MediaItem } from "../types"; import { getOriginalLanguage as radarrLang, type RadarrLibrary } from "./radarr"; import { getOriginalLanguage as sonarrLang, type SonarrLibrary } from "./sonarr"; export interface LanguageResolverConfig { radarr: { url: string; apiKey: string } | null; sonarr: { url: string; apiKey: string } | null; radarrLibrary: RadarrLibrary | null; sonarrLibrary: SonarrLibrary | null; resolveSeriesTvdb: ((seriesJellyfinId: string) => Promise) | null; } export interface LanguageResult { origLang: string | null; origLangSource: string | null; needsReview: number; externalRaw: unknown; } /** * Resolve the original language of a media item using Radarr (movies) or * Sonarr (episodes). Pure lookup — does NOT write to the database; the * caller decides what to persist. */ export async function resolveLanguage( db: Database, itemId: number, cfg: LanguageResolverConfig, ): Promise { const item = db.prepare("SELECT * FROM media_items WHERE id = ?").get(itemId) as MediaItem | null; if (!item) throw new Error(`media_items row ${itemId} not found`); // Manual override is sacred — never touch it if (item.orig_lang_source === "manual") { return { origLang: item.original_language, origLangSource: "manual", needsReview: item.needs_review, externalRaw: null, }; } if (item.type === "Movie") { return resolveMovie(item, cfg); } return resolveEpisode(item, cfg); } async function resolveMovie(item: MediaItem, cfg: LanguageResolverConfig): Promise { if (!cfg.radarr || !cfg.radarrLibrary) { return jellyfinFallback(item); } const lang = await radarrLang(cfg.radarr, { tmdbId: item.tmdb_id ?? undefined, imdbId: item.imdb_id ?? undefined }, cfg.radarrLibrary); if (lang) { // Capture the library entry for external_raw const entry = (item.tmdb_id ? cfg.radarrLibrary.byTmdbId.get(item.tmdb_id) : null) ?? (item.imdb_id ? cfg.radarrLibrary.byImdbId.get(item.imdb_id) : null) ?? null; return { origLang: lang, origLangSource: "radarr", needsReview: 0, externalRaw: entry, }; } return jellyfinFallback(item); } async function resolveEpisode(item: MediaItem, cfg: LanguageResolverConfig): Promise { if (!cfg.sonarr || !cfg.sonarrLibrary) { return jellyfinFallback(item); } let tvdbId = item.tvdb_id; // If the stored tvdbId doesn't match the library, it's likely an // episode-level ID. Try resolving the series TVDB BEFORE hitting the // Sonarr API — the HTTP fallback in sonarrLang is per-item and extremely // slow for bulk processing. if (tvdbId && !cfg.sonarrLibrary.byTvdbId.has(tvdbId) && cfg.resolveSeriesTvdb && item.series_jellyfin_id) { const seriesTvdb = await cfg.resolveSeriesTvdb(item.series_jellyfin_id); if (seriesTvdb) tvdbId = seriesTvdb; } if (tvdbId) { const lang = await sonarrLang(cfg.sonarr, tvdbId, cfg.sonarrLibrary); if (lang) { const entry = cfg.sonarrLibrary.byTvdbId.get(tvdbId) ?? null; return { origLang: lang, origLangSource: "sonarr", needsReview: 0, externalRaw: entry, }; } } return jellyfinFallback(item); } function jellyfinFallback(item: MediaItem): LanguageResult { return { origLang: item.original_language, origLangSource: item.orig_lang_source, needsReview: 1, externalRaw: null, }; }