Files
netfelix-audio-fix/server/services/__tests__/language-resolver.test.ts
T
2026-04-20 08:58:25 +02:00

194 lines
5.7 KiB
TypeScript

import { Database } from "bun:sqlite";
import { describe, expect, test, beforeEach } from "bun:test";
import { SCHEMA } from "../../db/schema";
import { resolveLanguage, type LanguageResolverConfig } from "../language-resolver";
import type { RadarrLibrary } from "../radarr";
import type { SonarrLibrary } from "../sonarr";
let db: Database;
function initDb(): Database {
const d = new Database(":memory:");
for (const stmt of SCHEMA.split(";").filter((s) => s.trim())) {
d.run(`${stmt};`);
}
return d;
}
function insertItem(db: Database, overrides: Record<string, unknown> = {}): number {
const defaults = {
jellyfin_id: "jf-1",
type: "Movie",
name: "Test Movie",
file_path: "/movies/test.mkv",
original_language: "deu",
orig_lang_source: "jellyfin",
needs_review: 0,
tmdb_id: "12345",
imdb_id: null,
tvdb_id: null,
series_jellyfin_id: null,
};
const row = { ...defaults, ...overrides };
db.prepare(
`INSERT INTO media_items (jellyfin_id, type, name, file_path, original_language, orig_lang_source, needs_review, tmdb_id, imdb_id, tvdb_id, series_jellyfin_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
).run(
row.jellyfin_id,
row.type,
row.name,
row.file_path,
row.original_language,
row.orig_lang_source,
row.needs_review,
row.tmdb_id,
row.imdb_id,
row.tvdb_id,
row.series_jellyfin_id,
);
return (db.prepare("SELECT id FROM media_items WHERE jellyfin_id = ?").get(row.jellyfin_id) as { id: number }).id;
}
function makeRadarrLibrary(entries: Array<{ tmdbId?: string; imdbId?: string; lang: string }>): RadarrLibrary {
const byTmdbId = new Map<string, { originalLanguage?: { name: string } }>();
const byImdbId = new Map<string, { originalLanguage?: { name: string } }>();
for (const e of entries) {
const movie = { originalLanguage: { name: e.lang } };
if (e.tmdbId) byTmdbId.set(e.tmdbId, movie);
if (e.imdbId) byImdbId.set(e.imdbId, movie);
}
return { byTmdbId, byImdbId };
}
function makeSonarrLibrary(entries: Array<{ tvdbId: string; lang: string }>): SonarrLibrary {
const byTvdbId = new Map<string, { originalLanguage?: { name: string } }>();
for (const e of entries) {
byTvdbId.set(e.tvdbId, { originalLanguage: { name: e.lang } });
}
return { byTvdbId };
}
function baseCfg(overrides: Partial<LanguageResolverConfig> = {}): LanguageResolverConfig {
return {
radarr: null,
sonarr: null,
radarrLibrary: null,
sonarrLibrary: null,
resolveSeriesTvdb: null,
...overrides,
};
}
beforeEach(() => {
db = initDb();
});
describe("resolveLanguage", () => {
test("radarr hit overrides jellyfin guess for movies", async () => {
const id = insertItem(db, {
type: "Movie",
tmdb_id: "12345",
original_language: "deu",
orig_lang_source: "jellyfin",
});
const cfg = baseCfg({
radarr: { url: "http://radarr:7878", apiKey: "key" },
radarrLibrary: makeRadarrLibrary([{ tmdbId: "12345", lang: "English" }]),
});
const result = await resolveLanguage(db, id, cfg);
expect(result.origLang).toBe("eng");
expect(result.origLangSource).toBe("radarr");
expect(result.needsReview).toBe(0);
expect(result.externalRaw).toBeDefined();
});
test("sonarr hit overrides jellyfin guess for episodes", async () => {
const id = insertItem(db, {
jellyfin_id: "jf-ep1",
type: "Episode",
name: "Test Episode",
tmdb_id: null,
tvdb_id: "99999",
original_language: "deu",
orig_lang_source: "jellyfin",
});
const cfg = baseCfg({
sonarr: { url: "http://sonarr:8989", apiKey: "key" },
sonarrLibrary: makeSonarrLibrary([{ tvdbId: "99999", lang: "Japanese" }]),
});
const result = await resolveLanguage(db, id, cfg);
expect(result.origLang).toBe("jpn");
expect(result.origLangSource).toBe("sonarr");
expect(result.needsReview).toBe(0);
});
test("resolveSeriesTvdb fixes episode with episode-level tvdbId", async () => {
const id = insertItem(db, {
jellyfin_id: "jf-ep2",
type: "Episode",
name: "Arrow S01E01",
tmdb_id: null,
tvdb_id: "episode-tvdb-123",
series_jellyfin_id: "series-jf-1",
original_language: "deu",
orig_lang_source: "jellyfin",
});
// Sonarr library has the series under a different tvdbId
const cfg = baseCfg({
sonarr: { url: "http://sonarr:8989", apiKey: "key" },
sonarrLibrary: makeSonarrLibrary([{ tvdbId: "series-tvdb-456", lang: "English" }]),
resolveSeriesTvdb: async (_seriesJellyfinId: string) => "series-tvdb-456",
});
const result = await resolveLanguage(db, id, cfg);
expect(result.origLang).toBe("eng");
expect(result.origLangSource).toBe("sonarr");
expect(result.needsReview).toBe(0);
});
test("skips resolution when orig_lang_source is manual", async () => {
const id = insertItem(db, {
type: "Movie",
tmdb_id: "12345",
original_language: "fra",
orig_lang_source: "manual",
});
const cfg = baseCfg({
radarr: { url: "http://radarr:7878", apiKey: "key" },
radarrLibrary: makeRadarrLibrary([{ tmdbId: "12345", lang: "English" }]),
});
const result = await resolveLanguage(db, id, cfg);
expect(result.origLang).toBe("fra");
expect(result.origLangSource).toBe("manual");
expect(result.needsReview).toBe(0);
});
test("returns jellyfin guess unchanged when no external source matches", async () => {
const id = insertItem(db, {
type: "Movie",
tmdb_id: "99999",
original_language: "deu",
orig_lang_source: "jellyfin",
needs_review: 0,
});
// Radarr library has no entry for tmdb 99999
const cfg = baseCfg({
radarr: { url: "http://radarr:7878", apiKey: "key" },
radarrLibrary: makeRadarrLibrary([{ tmdbId: "11111", lang: "English" }]),
});
const result = await resolveLanguage(db, id, cfg);
expect(result.origLang).toBe("deu");
expect(result.origLangSource).toBe("jellyfin");
expect(result.needsReview).toBe(1);
expect(result.externalRaw).toBeNull();
});
});