diff --git a/server/services/analyzer.ts b/server/services/analyzer.ts index abc4f08..e8c2130 100644 --- a/server/services/analyzer.ts +++ b/server/services/analyzer.ts @@ -1,7 +1,7 @@ import type { MediaItem, MediaStream, PlanResult } from "../types"; import { computeAppleCompat, isAppleCompatible, transcodeTarget } from "./apple-compat"; import { isExtractableSubtitle } from "./ffmpeg"; -import { normalizeLanguage } from "./jellyfin"; +import { normalizeLanguage } from "./language-utils"; const AUTHORITATIVE_ORIG_SOURCES = new Set(["radarr", "sonarr", "manual"]); diff --git a/server/services/ffmpeg.ts b/server/services/ffmpeg.ts index a698f00..ced5848 100644 --- a/server/services/ffmpeg.ts +++ b/server/services/ffmpeg.ts @@ -1,5 +1,5 @@ import type { MediaItem, MediaStream, StreamDecision } from "../types"; -import { normalizeLanguage } from "./jellyfin"; +import { normalizeLanguage } from "./language-utils"; // ─── Subtitle extraction helpers ────────────────────────────────────────────── diff --git a/server/services/jellyfin.ts b/server/services/jellyfin.ts index debda94..b83a5b3 100644 --- a/server/services/jellyfin.ts +++ b/server/services/jellyfin.ts @@ -1,4 +1,5 @@ import type { JellyfinItem, JellyfinMediaStream, JellyfinUser, MediaStream } from "../types"; +import { normalizeLanguage } from "./language-utils"; export interface JellyfinConfig { url: string; @@ -246,102 +247,4 @@ export function mapStream(s: JellyfinMediaStream): Omit = { - en: "eng", - de: "deu", - es: "spa", - fr: "fra", - it: "ita", - pt: "por", - ja: "jpn", - ko: "kor", - zh: "zho", - ar: "ara", - ru: "rus", - nl: "nld", - sv: "swe", - no: "nor", - da: "dan", - fi: "fin", - pl: "pol", - tr: "tur", - th: "tha", - hi: "hin", - hu: "hun", - cs: "ces", - ro: "ron", - el: "ell", - he: "heb", - fa: "fas", - uk: "ukr", - id: "ind", - ca: "cat", - nb: "nob", - nn: "nno", - is: "isl", - hr: "hrv", - sk: "slk", - bg: "bul", - sr: "srp", - sl: "slv", - lv: "lav", - lt: "lit", - et: "est", - vi: "vie", - ms: "msa", - ta: "tam", - te: "tel", -}; - -// ISO 639-2/T → ISO 639-2/B normalization + common aliases -const LANG_ALIASES: Record = { - // German: both /T (deu) and /B (ger) → deu - ger: "deu", - // Chinese - chi: "zho", - // French - fre: "fra", - // Dutch - dut: "nld", - // Modern Greek - gre: "ell", - // Hebrew - heb: "heb", - // Farsi - per: "fas", - // Romanian - rum: "ron", - // Malay - may: "msa", - // Tibetan - tib: "bod", - // Burmese - bur: "mya", - // Czech - cze: "ces", - // Slovak - slo: "slk", - // Georgian - geo: "kat", - // Icelandic - ice: "isl", - // Armenian - arm: "hye", - // Basque - baq: "eus", - // Albanian - alb: "sqi", - // Macedonian - mac: "mkd", - // Welsh - wel: "cym", -}; - -export function normalizeLanguage(lang: string): string { - const lower = lang.toLowerCase().trim(); - if (ISO_1_TO_2[lower]) return ISO_1_TO_2[lower]; - return LANG_ALIASES[lower] ?? lower; -} +export { normalizeLanguage } from "./language-utils"; diff --git a/server/services/language-utils.ts b/server/services/language-utils.ts new file mode 100644 index 0000000..72b9823 --- /dev/null +++ b/server/services/language-utils.ts @@ -0,0 +1,40 @@ +// ISO 639-1 (2-letter) → ISO 639-2/B (3-letter) +const ISO_1_TO_2: Record = { + en: "eng", de: "deu", es: "spa", fr: "fra", it: "ita", pt: "por", + ja: "jpn", ko: "kor", zh: "zho", ar: "ara", ru: "rus", nl: "nld", + sv: "swe", no: "nor", da: "dan", fi: "fin", pl: "pol", tr: "tur", + th: "tha", hi: "hin", hu: "hun", cs: "ces", ro: "ron", el: "ell", + he: "heb", fa: "fas", uk: "ukr", id: "ind", ca: "cat", nb: "nob", + nn: "nno", is: "isl", hr: "hrv", sk: "slk", bg: "bul", sr: "srp", + sl: "slv", lv: "lav", lt: "lit", et: "est", vi: "vie", ms: "msa", + ta: "tam", te: "tel", +}; + +// ISO 639-2/T → ISO 639-2/B normalization + common aliases +const LANG_ALIASES: Record = { + ger: "deu", chi: "zho", fre: "fra", dut: "nld", gre: "ell", + heb: "heb", per: "fas", rum: "ron", may: "msa", tib: "bod", + bur: "mya", cze: "ces", slo: "slk", geo: "kat", ice: "isl", + arm: "hye", baq: "eus", alb: "sqi", mac: "mkd", wel: "cym", +}; + +export function normalizeLanguage(lang: string): string { + const lower = lang.toLowerCase().trim(); + if (ISO_1_TO_2[lower]) return ISO_1_TO_2[lower]; + return LANG_ALIASES[lower] ?? lower; +} + +const DUB_TITLE_HINTS = /(dub|dubb|synchro|commentary|director)/i; + +/** + * Guess original language from audio streams by looking at the default track. + * Heuristic: prefer the default audio track, skip dubs/commentary, fall back to first. + */ +export function guessOriginalLanguage( + audioStreams: { language: string | null; title: string | null; isDefault: number }[], +): string | null { + if (audioStreams.length === 0) return null; + const notDub = (s: { title: string | null }) => !s.title || !DUB_TITLE_HINTS.test(s.title); + const pick = audioStreams.find((s) => s.isDefault && notDub(s)) ?? audioStreams.find(notDub) ?? audioStreams[0]; + return pick.language ? normalizeLanguage(pick.language) : null; +} diff --git a/server/services/radarr.ts b/server/services/radarr.ts index 642c0ca..6ca6b13 100644 --- a/server/services/radarr.ts +++ b/server/services/radarr.ts @@ -1,5 +1,5 @@ import { error as logError, warn } from "../lib/log"; -import { normalizeLanguage } from "./jellyfin"; +import { normalizeLanguage } from "./language-utils"; export interface RadarrConfig { url: string; diff --git a/server/services/sonarr.ts b/server/services/sonarr.ts index 11af6c0..4c6b2f0 100644 --- a/server/services/sonarr.ts +++ b/server/services/sonarr.ts @@ -1,5 +1,5 @@ import { error as logError, warn } from "../lib/log"; -import { normalizeLanguage } from "./jellyfin"; +import { normalizeLanguage } from "./language-utils"; export interface SonarrConfig { url: string;