aafe5b2ec1
Build and Push Docker Image / build (push) Successful in 1m9s
the resolver was calling sonarrLang with episode-level TVDB IDs first, missing the library, triggering an HTTP round-trip per episode. now checks the library and resolves the correct series TVDB before calling sonarrLang. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
113 lines
3.3 KiB
TypeScript
113 lines
3.3 KiB
TypeScript
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<string | null>) | 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<LanguageResult> {
|
|
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<LanguageResult> {
|
|
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<LanguageResult> {
|
|
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,
|
|
};
|
|
}
|