Files
netfelix-audio-fix/server/services/language-resolver.ts
T
felixfoertsch 6022ed09b2 simplify language-resolver: drop jellyfin resolveSeriesTvdb callback
remove resolveSeriesTvdb from LanguageResolverConfig, rename jellyfinFallback
to probeFallback, replace Jellyfin-based TVDB resolution with series_name
title search over Sonarr library, update tests accordingly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 06:31:54 +02:00

106 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;
}
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 probeFallback(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 probeFallback(item);
}
async function resolveEpisode(item: MediaItem, cfg: LanguageResolverConfig): Promise<LanguageResult> {
if (!cfg.sonarr || !cfg.sonarrLibrary) return probeFallback(item);
// Try direct TVDB ID lookup (from path parser's [tvdbid-X] in folder name)
if (item.tvdb_id) {
const lang = await sonarrLang(cfg.sonarr, item.tvdb_id, cfg.sonarrLibrary);
if (lang) {
const entry = cfg.sonarrLibrary.byTvdbId.get(item.tvdb_id) ?? null;
return { origLang: lang, origLangSource: "sonarr", needsReview: 0, externalRaw: entry };
}
}
// Fallback: search Sonarr library by series name
if (item.series_name) {
const lowerName = item.series_name.toLowerCase();
for (const [tvdbId, series] of cfg.sonarrLibrary.byTvdbId) {
if ((series as any).title?.toLowerCase() === lowerName) {
const lang = await sonarrLang(cfg.sonarr, tvdbId, cfg.sonarrLibrary);
if (lang) return { origLang: lang, origLangSource: "sonarr", needsReview: 0, externalRaw: series };
}
}
}
return probeFallback(item);
}
function probeFallback(item: MediaItem): LanguageResult {
return {
origLang: item.original_language,
origLangSource: item.orig_lang_source,
needsReview: 1,
externalRaw: null,
};
}