Files
netfelix-audio-fix/server/services/__tests__/analyzer.test.ts
T
felixfoertsch 748145a372
Build and Push Docker Image / build (push) Successful in 3m57s
detect dirty container title and comment, rewrite to canonical form
Track format.tags.title and format.tags.comment on media_items via a new
containerTitle() helper producing "Name (Year)" for movies and
"Series (Year) - S01E02 - Title" for episodes. Analyzer and
recomputePlanAfterToggle now flag non-canonical container title and
non-empty comment as non-noop ("Fix container title", "Clear comment"),
and verifyDesiredState checks them post-ffmpeg. buildStreamFlags writes
the canonical title and clears comment on every run.

Existing libraries need a rescan to populate the new columns.
2026-04-24 21:45:39 +02:00

744 lines
28 KiB
TypeScript

import { describe, expect, test } from "bun:test";
import type { MediaItem, MediaStream } from "../../types";
import { analyzeItem } from "../analyzer";
type OrigLangSource = MediaItem["orig_lang_source"];
type StreamOverride = Partial<MediaStream> & Pick<MediaStream, "id" | "type" | "stream_index">;
function stream(o: StreamOverride): MediaStream {
return {
item_id: 1,
codec: null,
profile: null,
language: null,
title: null,
is_default: 0,
is_forced: 0,
is_hearing_impaired: 0,
channels: null,
channel_layout: null,
bit_rate: null,
sample_rate: null,
bit_depth: null,
width: null,
height: null,
...o,
};
}
const ITEM_DEFAULTS = {
needs_review: 0 as number,
container: "mkv" as string | null,
orig_lang_source: null as OrigLangSource,
// Default to a clean movie with matching canonical container title so
// existing noop tests stay noop. Tests that care about container title
// detection override these explicitly.
type: "Movie" as "Movie" | "Episode",
name: "Test" as string,
year: null as number | null,
series_name: null as string | null,
season_number: null as number | null,
episode_number: null as number | null,
container_title: "Test" as string | null,
container_comment: null as string | null,
};
describe("analyzeItem — audio keep rules", () => {
test("keeps only OG + configured languages, drops others", () => {
const streams = [
stream({ id: 1, type: "Video", stream_index: 0, codec: "h264" }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "aac", language: "eng" }),
stream({ id: 3, type: "Audio", stream_index: 2, codec: "aac", language: "deu" }),
stream({ id: 4, type: "Audio", stream_index: 3, codec: "aac", language: "fra" }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, {
audioLanguages: ["deu"],
});
const actions = Object.fromEntries(result.decisions.map((d) => [d.stream_id, d.action]));
expect(actions).toEqual({ 1: "keep", 2: "keep", 3: "keep", 4: "remove" });
});
test("keeps all audio when OG language unknown", () => {
const streams = [
stream({ id: 1, type: "Audio", stream_index: 0, language: "eng" }),
stream({ id: 2, type: "Audio", stream_index: 1, language: "deu" }),
stream({ id: 3, type: "Audio", stream_index: 2, language: "fra" }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: null, needs_review: 1 }, streams, {
audioLanguages: ["deu"],
});
expect(result.decisions.every((d) => d.action === "keep")).toBe(true);
expect(result.notes.some((n) => n.includes("manual review"))).toBe(true);
});
test("keeps audio tracks with undetermined language", () => {
const streams = [
stream({ id: 1, type: "Audio", stream_index: 0, language: "eng" }),
stream({ id: 2, type: "Audio", stream_index: 1, language: null }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, {
audioLanguages: [],
});
const byId = Object.fromEntries(result.decisions.map((d) => [d.stream_id, d.action]));
expect(byId[2]).toBe("keep");
});
test("normalizes language codes (ger → deu)", () => {
const streams = [stream({ id: 1, type: "Audio", stream_index: 0, language: "ger" })];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "deu" }, streams, {
audioLanguages: [],
});
expect(result.decisions[0].action).toBe("keep");
});
test("custom_language override wins over stream.language for keep/remove", () => {
// File says UND, user corrects it to Spanish. With OG=eng and no extra
// keep languages, the track should be removed — the override flowed
// into the decision just like a real "spa" tag would have.
const streams = [
stream({ id: 1, type: "Video", stream_index: 0 }),
stream({ id: 2, type: "Audio", stream_index: 1, language: null }),
];
const overrides = new Map<number, string>([[2, "spa"]]);
const removing = analyzeItem(
{ ...ITEM_DEFAULTS, original_language: "eng" },
streams,
{ audioLanguages: [] },
overrides,
);
expect(removing.decisions.find((d) => d.stream_id === 2)?.action).toBe("remove");
// Same file, but Spanish is now in the keep list → kept.
const keeping = analyzeItem(
{ ...ITEM_DEFAULTS, original_language: "eng" },
streams,
{ audioLanguages: ["spa"] },
overrides,
);
expect(keeping.decisions.find((d) => d.stream_id === 2)?.action).toBe("keep");
});
});
describe("analyzeItem — audio ordering", () => {
test("OG first, then additional languages in configured order", () => {
const streams = [
stream({ id: 10, type: "Audio", stream_index: 0, codec: "aac", language: "deu" }),
stream({ id: 11, type: "Audio", stream_index: 1, codec: "aac", language: "eng" }),
stream({ id: 12, type: "Audio", stream_index: 2, codec: "aac", language: "spa" }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, {
audioLanguages: ["deu", "spa"],
});
const byId = Object.fromEntries(result.decisions.map((d) => [d.stream_id, d.target_index]));
expect(byId[11]).toBe(0); // eng (OG) first
expect(byId[10]).toBe(1); // deu second
expect(byId[12]).toBe(2); // spa third
});
test("audioOrderChanged is_noop=false when OG audio is not first in input", () => {
const streams = [
stream({ id: 1, type: "Video", stream_index: 0, codec: "h264" }),
stream({ id: 2, type: "Audio", stream_index: 1, language: "deu" }),
stream({ id: 3, type: "Audio", stream_index: 2, language: "eng" }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, {
audioLanguages: ["deu"],
});
expect(result.is_noop).toBe(false);
});
test("audioOrderChanged is_noop=true when OG audio is already first and default", () => {
const streams = [
stream({ id: 1, type: "Video", stream_index: 0, codec: "h264" }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "aac", language: "eng", is_default: 1, title: "ENG - AAC" }),
stream({ id: 3, type: "Audio", stream_index: 2, codec: "aac", language: "deu", title: "DEU - AAC" }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, {
audioLanguages: ["deu"],
});
expect(result.is_noop).toBe(true);
});
test("removing an audio track triggers non-noop even if OG first", () => {
const streams = [
stream({ id: 1, type: "Audio", stream_index: 0, codec: "aac", language: "eng" }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "aac", language: "fra" }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, {
audioLanguages: [],
});
expect(result.is_noop).toBe(false);
});
});
describe("analyzeItem — subtitles & is_noop", () => {
test("subtitles are always marked remove (extracted to sidecar)", () => {
const streams = [
stream({ id: 1, type: "Audio", stream_index: 0, codec: "aac", language: "eng" }),
stream({ id: 2, type: "Subtitle", stream_index: 1, codec: "subrip", language: "eng" }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, {
audioLanguages: [],
});
const subDec = result.decisions.find((d) => d.stream_id === 2);
expect(subDec?.action).toBe("remove");
expect(result.is_noop).toBe(false); // subs present → not noop
});
test("no audio change, no subs, OG already default+canonical → is_noop true", () => {
const streams = [
stream({ id: 1, type: "Video", stream_index: 0, codec: "h264" }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "aac", language: "eng", is_default: 1, title: "ENG - AAC" }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, {
audioLanguages: [],
});
expect(result.is_noop).toBe(true);
});
test("OG audio present but not default → is_noop false (pipeline would set default)", () => {
const streams = [
stream({ id: 1, type: "Video", stream_index: 0, codec: "h264" }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "aac", language: "eng", is_default: 0 }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, {
audioLanguages: [],
});
expect(result.is_noop).toBe(false);
});
test("non-canonical language tag (en instead of eng) → is_noop false", () => {
const streams = [
stream({ id: 1, type: "Video", stream_index: 0, codec: "h264" }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "aac", language: "en", is_default: 1 }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, {
audioLanguages: [],
});
expect(result.is_noop).toBe(false);
});
});
describe("analyzeItem — transcode targets", () => {
test("DTS on mp4 → transcode to eac3", () => {
const streams = [stream({ id: 1, type: "Audio", stream_index: 0, codec: "dts", language: "eng" })];
const result = analyzeItem(
{ ...ITEM_DEFAULTS, original_language: "eng", needs_review: 0, container: "mp4" },
streams,
{ audioLanguages: [] },
);
expect(result.decisions[0].transcode_codec).toBe("eac3");
expect(result.job_type).toBe("transcode");
expect(result.is_noop).toBe(false);
});
test("AAC passes through without transcode", () => {
const streams = [stream({ id: 1, type: "Audio", stream_index: 0, codec: "aac", language: "eng" })];
const result = analyzeItem(
{ ...ITEM_DEFAULTS, original_language: "eng", needs_review: 0, container: "mp4" },
streams,
{ audioLanguages: [] },
);
expect(result.decisions[0].transcode_codec).toBe(null);
expect(result.job_type).toBe("copy");
});
});
describe("analyzeItem — one audio track per language", () => {
test("drops commentary track even when it matches OG language", () => {
const streams = [
stream({ id: 1, type: "Video", stream_index: 0, codec: "h264" }),
stream({
id: 2,
type: "Audio",
stream_index: 1,
codec: "ac3",
language: "eng",
is_default: 1,
channels: 6,
title: "Surround 5.1",
}),
stream({
id: 3,
type: "Audio",
stream_index: 2,
codec: "ac3",
language: "eng",
channels: 2,
title: "Director's Commentary",
}),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, { audioLanguages: [] });
const actions = Object.fromEntries(result.decisions.map((d) => [d.stream_id, d.action]));
expect(actions).toEqual({ 1: "keep", 2: "keep", 3: "remove" });
});
test("drops audio-description (hearing-impaired) track", () => {
const streams = [
stream({ id: 1, type: "Audio", stream_index: 0, codec: "ac3", language: "eng", is_default: 1, channels: 6 }),
stream({
id: 2,
type: "Audio",
stream_index: 1,
codec: "ac3",
language: "eng",
channels: 6,
is_hearing_impaired: 1,
title: "Audio Description",
}),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, { audioLanguages: [] });
const actions = Object.fromEntries(result.decisions.map((d) => [d.stream_id, d.action]));
expect(actions).toEqual({ 1: "keep", 2: "remove" });
});
test("keeps the default track when two same-language tracks exist", () => {
const streams = [
stream({ id: 1, type: "Audio", stream_index: 0, codec: "aac", language: "eng", channels: 2 }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "ac3", language: "eng", channels: 6, is_default: 1 }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, { audioLanguages: [] });
const actions = Object.fromEntries(result.decisions.map((d) => [d.stream_id, d.action]));
expect(actions).toEqual({ 1: "remove", 2: "keep" });
});
test("when neither track is default, prefers more channels", () => {
const streams = [
stream({ id: 1, type: "Audio", stream_index: 0, codec: "aac", language: "eng", channels: 2 }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "dts", language: "eng", channels: 6 }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, { audioLanguages: [] });
const actions = Object.fromEntries(result.decisions.map((d) => [d.stream_id, d.action]));
expect(actions).toEqual({ 1: "remove", 2: "keep" });
});
test("Apple-compatible AC3 wins over DTS-HD MA default at same channel count", () => {
// The lossless DTS track being 'default' used to force a transcode;
// with Apple-compat above default in the priority order, we pick the
// AC3 track and the job becomes a pure copy.
const streams = [
stream({
id: 1,
type: "Audio",
stream_index: 0,
codec: "dts",
profile: "DTS-HD MA",
language: "eng",
is_default: 1,
channels: 6,
}),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "ac3", language: "eng", channels: 6 }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng", container: "mkv" }, streams, {
audioLanguages: [],
});
const actions = Object.fromEntries(result.decisions.map((d) => [d.stream_id, d.action]));
expect(actions).toEqual({ 1: "remove", 2: "keep" });
expect(result.job_type).toBe("copy"); // no transcode needed
});
test("higher channels wins over Apple-compat (7.1 TrueHD beats 5.1 AC3)", () => {
const streams = [
stream({ id: 1, type: "Audio", stream_index: 0, codec: "truehd", language: "eng", channels: 8 }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "ac3", language: "eng", channels: 6, is_default: 1 }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng", container: "mkv" }, streams, {
audioLanguages: [],
});
const actions = Object.fromEntries(result.decisions.map((d) => [d.stream_id, d.action]));
expect(actions).toEqual({ 1: "keep", 2: "remove" });
expect(result.decisions[0].transcode_codec).toBe("eac3");
});
test("with equal channels + no default, prefers Apple-compatible over DTS", () => {
const streams = [
stream({ id: 1, type: "Audio", stream_index: 0, codec: "dts", language: "eng", channels: 6 }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "aac", language: "eng", channels: 6 }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, { audioLanguages: [] });
const actions = Object.fromEntries(result.decisions.map((d) => [d.stream_id, d.action]));
expect(actions).toEqual({ 1: "remove", 2: "keep" });
});
test("dedupes within each language independently (OG English + extra German)", () => {
const streams = [
stream({ id: 1, type: "Audio", stream_index: 0, codec: "ac3", language: "eng", is_default: 1, channels: 6 }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "aac", language: "eng", channels: 2 }),
stream({ id: 3, type: "Audio", stream_index: 2, codec: "ac3", language: "deu", channels: 6, is_default: 1 }),
stream({
id: 4,
type: "Audio",
stream_index: 3,
codec: "aac",
language: "deu",
channels: 2,
title: "Kommentar",
}),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng" }, streams, { audioLanguages: ["deu"] });
const actions = Object.fromEntries(result.decisions.map((d) => [d.stream_id, d.action]));
// One English (default surround wins), one German (default wins, commentary
// is still a valid alternate because the title is in a language the regex
// doesn't know — but the default wins by disposition anyway).
expect(actions).toEqual({ 1: "keep", 2: "remove", 3: "keep", 4: "remove" });
});
test("single-stream file stays a noop", () => {
const streams = [
stream({ id: 1, type: "Video", stream_index: 0, codec: "h264" }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "aac", language: "eng", is_default: 1, title: "ENG - AAC" }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng", container: "mp4" }, streams, {
audioLanguages: [],
});
expect(result.is_noop).toBe(true);
});
test("wrong audio title (e.g. 'Chinese - Dolby Digital - 5.1') → not noop", () => {
const streams = [
stream({ id: 1, type: "Video", stream_index: 0, codec: "h264" }),
stream({
id: 2,
type: "Audio",
stream_index: 1,
codec: "eac3",
language: "zho",
channels: 6,
is_default: 1,
title: "Chinese - Dolby Digital Plus - 5.1 - Default",
}),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "zho" }, streams, { audioLanguages: [] });
expect(result.is_noop).toBe(false);
expect(result.reasons).toContain("Fix audio title");
});
test("correct harmonized title → noop", () => {
const streams = [
stream({ id: 1, type: "Video", stream_index: 0, codec: "h264" }),
stream({
id: 2,
type: "Audio",
stream_index: 1,
codec: "eac3",
language: "zho",
channels: 6,
is_default: 1,
title: "ZHO - EAC3 5.1",
}),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "zho" }, streams, { audioLanguages: [] });
expect(result.is_noop).toBe(true);
});
test("null title → not noop (title needs to be set)", () => {
const streams = [
stream({ id: 1, type: "Video", stream_index: 0, codec: "h264" }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "aac", language: "eng", is_default: 1, title: null }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng", container: "mp4" }, streams, {
audioLanguages: [],
});
expect(result.is_noop).toBe(false);
});
test("wrong video title with release/ad noise → not noop", () => {
const streams = [
stream({
id: 1,
type: "Video",
stream_index: 0,
codec: "h264",
width: 1920,
height: 1080,
title: "Movie.Name.1080p.WEB-DL.ADS-GRP",
}),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "aac", language: "eng", is_default: 1, title: "ENG - AAC" }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng", container: "mp4" }, streams, {
audioLanguages: [],
});
expect(result.is_noop).toBe(false);
expect(result.reasons).toContain("Fix video title");
});
test("correct video and audio titles → noop", () => {
const streams = [
stream({
id: 1,
type: "Video",
stream_index: 0,
codec: "h264",
width: 1920,
height: 1080,
title: "1080p - H.264",
}),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "aac", language: "eng", is_default: 1, title: "ENG - AAC" }),
];
const result = analyzeItem({ ...ITEM_DEFAULTS, original_language: "eng", container: "mp4" }, streams, {
audioLanguages: [],
});
expect(result.is_noop).toBe(true);
});
test("dirty container title with release-group junk → not noop", () => {
const streams = [
stream({ id: 1, type: "Video", stream_index: 0, codec: "h264", width: 1920, height: 1080, title: "1080p - H.264" }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "aac", language: "eng", is_default: 1, title: "ENG - AAC" }),
];
const result = analyzeItem(
{
...ITEM_DEFAULTS,
type: "Movie",
name: "101 Dalmatians",
year: 1961,
container_title: "101.Dalmatians.1961.1080p.BluRay.x264-RARBG",
original_language: "eng",
},
streams,
{ audioLanguages: [] },
);
expect(result.is_noop).toBe(false);
expect(result.reasons).toContain("Fix container title");
});
test("container title matching canonical form → noop", () => {
const streams = [
stream({ id: 1, type: "Video", stream_index: 0, codec: "h264", width: 1920, height: 1080, title: "1080p - H.264" }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "aac", language: "eng", is_default: 1, title: "ENG - AAC" }),
];
const result = analyzeItem(
{
...ITEM_DEFAULTS,
type: "Movie",
name: "101 Dalmatians",
year: 1961,
container_title: "101 Dalmatians (1961)",
original_language: "eng",
},
streams,
{ audioLanguages: [] },
);
expect(result.is_noop).toBe(true);
});
test("non-empty container comment → not noop with reason Clear comment", () => {
const streams = [
stream({ id: 1, type: "Video", stream_index: 0, codec: "h264", width: 1920, height: 1080, title: "1080p - H.264" }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "aac", language: "eng", is_default: 1, title: "ENG - AAC" }),
];
const result = analyzeItem(
{
...ITEM_DEFAULTS,
type: "Movie",
name: "101 Dalmatians",
year: 1961,
container_title: "101 Dalmatians (1961)",
container_comment: "rarbg",
original_language: "eng",
},
streams,
{ audioLanguages: [] },
);
expect(result.is_noop).toBe(false);
expect(result.reasons).toContain("Clear comment");
});
});
describe("analyzeItem — auto_class classification", () => {
const AUTHORITATIVE = {
...ITEM_DEFAULTS,
original_language: "eng" as string | null,
orig_lang_source: "radarr" as OrigLangSource,
needs_review: 0,
};
test("one OG language track → auto", () => {
const streams = [
stream({ id: 1, type: "Video", stream_index: 0, codec: "h264" }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "eac3", language: "eng", channels: 6 }),
];
const result = analyzeItem(AUTHORITATIVE, streams, { audioLanguages: [] });
expect(result.auto_class).toBe("auto");
});
test("OG + additional configured language, both kept → auto", () => {
const streams = [
stream({ id: 1, type: "Video", stream_index: 0, codec: "h264" }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "eac3", language: "eng", channels: 6 }),
stream({ id: 3, type: "Audio", stream_index: 2, codec: "eac3", language: "deu", channels: 6 }),
];
const result = analyzeItem(AUTHORITATIVE, streams, { audioLanguages: ["deu"] });
expect(result.auto_class).toBe("auto");
});
test("two English tracks resolved by channel count → auto", () => {
const streams = [
stream({ id: 1, type: "Video", stream_index: 0, codec: "h264" }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "dts", language: "eng", channels: 6, title: "English 5.1" }),
stream({
id: 3,
type: "Audio",
stream_index: 2,
codec: "ac3",
language: "eng",
channels: 2,
title: "English Stereo",
}),
];
const result = analyzeItem(AUTHORITATIVE, streams, { audioLanguages: [] });
expect(result.auto_class).toBe("auto");
});
test("commentary track dropped by title heuristic → auto_heuristic", () => {
const streams = [
stream({ id: 1, type: "Video", stream_index: 0, codec: "h264" }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "eac3", language: "eng", channels: 6, title: "English 5.1" }),
stream({
id: 3,
type: "Audio",
stream_index: 2,
codec: "ac3",
language: "eng",
channels: 2,
title: "Director's Commentary",
}),
];
const result = analyzeItem(AUTHORITATIVE, streams, { audioLanguages: [] });
expect(result.auto_class).toBe("auto_heuristic");
});
test("OG language unknown → manual", () => {
const streams = [stream({ id: 1, type: "Audio", stream_index: 0, codec: "eac3", language: "eng" })];
const result = analyzeItem(
{ ...ITEM_DEFAULTS, original_language: null, orig_lang_source: null, needs_review: 1 },
streams,
{ audioLanguages: [] },
);
expect(result.auto_class).toBe("manual");
});
test("OG known but not present in any audio track → manual", () => {
const streams = [stream({ id: 1, type: "Audio", stream_index: 0, codec: "eac3", language: "deu" })];
const result = analyzeItem(
{ ...ITEM_DEFAULTS, original_language: "eng", orig_lang_source: "radarr", needs_review: 0 },
streams,
{ audioLanguages: [] },
);
expect(result.auto_class).toBe("manual");
});
test("kept audio track with null language tag → manual", () => {
const streams = [
stream({ id: 1, type: "Audio", stream_index: 0, codec: "eac3", language: "eng" }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "eac3", language: null }),
];
const result = analyzeItem(
{ ...ITEM_DEFAULTS, original_language: "eng", orig_lang_source: "radarr", needs_review: 0 },
streams,
{ audioLanguages: [] },
);
expect(result.auto_class).toBe("manual");
});
test("needs_review=1 → manual even with known OG", () => {
const streams = [stream({ id: 1, type: "Audio", stream_index: 0, codec: "eac3", language: "eng" })];
const result = analyzeItem(
{ ...ITEM_DEFAULTS, original_language: "eng", orig_lang_source: "radarr", needs_review: 1 },
streams,
{ audioLanguages: [] },
);
expect(result.auto_class).toBe("manual");
});
test("non-OG track with coincidental commentary-ish title → auto (not auto_heuristic)", () => {
// OG is English. German track is removed for LANGUAGE reasons, not title.
// Its title coincidentally contains 'commentary', but that should not
// upgrade the classification to auto_heuristic.
const streams = [
stream({ id: 1, type: "Video", stream_index: 0, codec: "h264" }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "eac3", language: "eng", channels: 6 }),
stream({
id: 3,
type: "Audio",
stream_index: 2,
codec: "ac3",
language: "deu",
channels: 2,
title: "German Commentary Audio",
}),
];
const result = analyzeItem(AUTHORITATIVE, streams, { audioLanguages: [] });
expect(result.auto_class).toBe("auto");
});
test("orig_lang_source=jellyfin is not authoritative → manual", () => {
const streams = [stream({ id: 1, type: "Audio", stream_index: 0, codec: "eac3", language: "eng" })];
const result = analyzeItem(
{ ...ITEM_DEFAULTS, original_language: "eng", orig_lang_source: "probe", needs_review: 0 },
streams,
{ audioLanguages: [] },
);
expect(result.auto_class).toBe("manual");
});
});
describe("auto_class — OG quality inferior to non-OG", () => {
test("OG mono + non-OG 5.1 → auto_heuristic, not auto", () => {
const streams = [
stream({ id: 1, type: "Audio", stream_index: 0, codec: "aac", language: "jpn", channels: 1 }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "eac3", language: "eng", channels: 6 }),
];
const result = analyzeItem(
{ ...ITEM_DEFAULTS, original_language: "jpn", orig_lang_source: "sonarr", needs_review: 0 },
streams,
{ audioLanguages: ["eng"] },
);
expect(result.auto_class).toBe("auto_heuristic");
});
test("OG 5.1 + non-OG stereo → auto (OG is better)", () => {
const streams = [
stream({ id: 1, type: "Audio", stream_index: 0, codec: "eac3", language: "jpn", channels: 6 }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "aac", language: "eng", channels: 2 }),
];
const result = analyzeItem(
{ ...ITEM_DEFAULTS, original_language: "jpn", orig_lang_source: "sonarr", needs_review: 0 },
streams,
{ audioLanguages: ["eng"] },
);
expect(result.auto_class).toBe("auto");
});
test("OG and non-OG same channels → auto (no quality mismatch)", () => {
const streams = [
stream({ id: 1, type: "Audio", stream_index: 0, codec: "eac3", language: "jpn", channels: 6 }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "eac3", language: "eng", channels: 6 }),
];
const result = analyzeItem(
{ ...ITEM_DEFAULTS, original_language: "jpn", orig_lang_source: "sonarr", needs_review: 0 },
streams,
{ audioLanguages: ["eng"] },
);
expect(result.auto_class).toBe("auto");
});
test("non-OG removed by config but still triggers heuristic (Dead Zone case)", () => {
// Japanese mono OG, English 5.1 available but not in audio_languages.
// The analyzer removes English, but the quality gap should still flag
// for review — the user might want to keep the superior dub.
const streams = [
stream({ id: 1, type: "Audio", stream_index: 0, codec: "aac", language: "eng", channels: 6 }),
stream({ id: 2, type: "Audio", stream_index: 1, codec: "aac", language: "eng", channels: 6 }),
stream({ id: 3, type: "Audio", stream_index: 2, codec: "aac", language: "jpn", channels: 1 }),
];
const result = analyzeItem(
{ ...ITEM_DEFAULTS, original_language: "jpn", orig_lang_source: "sonarr", needs_review: 0 },
streams,
{ audioLanguages: [] }, // English NOT in config
);
expect(result.auto_class).toBe("auto_heuristic");
});
});