All checks were successful
Build and Push Docker Image / build (push) Successful in 25s
The real reason 8 Mile landed as Turkish: Radarr WAS being called, but the
call path had three silent failure modes that all looked identical from
outside.
1. try { … } catch { return null } swallowed every error. No log when
Radarr was unreachable, when the API key was wrong, when HTTP returned
404/500, or when JSON parsing failed. A miss and a crash looked the
same: null, fall back to Jellyfin's dub guess.
2. /api/v3/movie?tmdbId=X only queries Radarr's LIBRARY. If the movie is
on disk + in Jellyfin but not actively managed in Radarr, returns [].
We then gave up and used the Jellyfin guess.
3. iso6391To6392 fell back to normalizeLanguage(name.slice(0, 3)) for any
unknown language name — pretending 'Mandarin' → 'man' and 'Flemish' →
'fle' are valid ISO 639-2 codes.
Fixes:
- Both services: fetchJson helper logs HTTP errors with context and the
url (api key redacted), plus catches+logs thrown errors.
- Added a metadata-lookup fallback: /api/v3/movie/lookup/tmdb and
/lookup/imdb for Radarr, /api/v3/series/lookup?term=tvdb:X for Sonarr.
These hit TMDB/TVDB via the arr service for titles not in its library.
- Expanded NAME_TO_639_2: Mandarin/Cantonese → zho, Flemish → nld,
Farsi → fas, plus common European langs that were missing.
- Unknown name → return null (log a warning) instead of a made-up 3-char
code. scan.ts then marks needs_review.
- scan.ts: per-item warn when Radarr/Sonarr miss; per-scan summary line
showing hits/misses/no-provider-id tallies.
Run a scan — the logs will now tell you whether Radarr was called, what
it answered, and why it fell back if it did.
119 lines
3.0 KiB
TypeScript
119 lines
3.0 KiB
TypeScript
import { error as logError, warn } from "../lib/log";
|
|
import { normalizeLanguage } from "./jellyfin";
|
|
|
|
export interface SonarrConfig {
|
|
url: string;
|
|
apiKey: string;
|
|
}
|
|
|
|
function headers(apiKey: string): Record<string, string> {
|
|
return { "X-Api-Key": apiKey };
|
|
}
|
|
|
|
export async function testConnection(cfg: SonarrConfig): Promise<{ ok: boolean; error?: string }> {
|
|
try {
|
|
const res = await fetch(`${cfg.url}/api/v3/system/status`, {
|
|
headers: headers(cfg.apiKey),
|
|
});
|
|
if (!res.ok) return { ok: false, error: `HTTP ${res.status}` };
|
|
return { ok: true };
|
|
} catch (e) {
|
|
return { ok: false, error: String(e) };
|
|
}
|
|
}
|
|
|
|
interface SonarrSeries {
|
|
tvdbId?: number;
|
|
originalLanguage?: { name: string };
|
|
}
|
|
|
|
async function fetchJson<T>(url: string, cfg: SonarrConfig, context: string): Promise<T | null> {
|
|
try {
|
|
const res = await fetch(url, { headers: headers(cfg.apiKey) });
|
|
if (!res.ok) {
|
|
warn(`Sonarr ${context} → HTTP ${res.status} (${url.replace(cfg.apiKey, "***")})`);
|
|
return null;
|
|
}
|
|
return (await res.json()) as T;
|
|
} catch (e) {
|
|
logError(`Sonarr ${context} failed:`, e);
|
|
return null;
|
|
}
|
|
}
|
|
|
|
/** Returns ISO 639-2 original language for a series or null. */
|
|
export async function getOriginalLanguage(cfg: SonarrConfig, tvdbId: string): Promise<string | null> {
|
|
// 1. Library: only hits series actively managed in Sonarr.
|
|
const list = await fetchJson<SonarrSeries[]>(`${cfg.url}/api/v3/series?tvdbId=${tvdbId}`, cfg, "library/tvdb");
|
|
const series = list?.[0];
|
|
if (series?.originalLanguage) return nameToIso(series.originalLanguage.name);
|
|
|
|
// 2. Metadata lookup via Sonarr's TVDB bridge for series not in the library.
|
|
const lookup = await fetchJson<SonarrSeries[]>(
|
|
`${cfg.url}/api/v3/series/lookup?term=tvdb%3A${tvdbId}`,
|
|
cfg,
|
|
"lookup/tvdb",
|
|
);
|
|
const fromLookup = lookup?.find((s) => String(s.tvdbId ?? "") === String(tvdbId)) ?? lookup?.[0];
|
|
if (fromLookup?.originalLanguage) return nameToIso(fromLookup.originalLanguage.name);
|
|
|
|
return null;
|
|
}
|
|
|
|
const NAME_TO_639_2: Record<string, string> = {
|
|
english: "eng",
|
|
french: "fra",
|
|
german: "deu",
|
|
spanish: "spa",
|
|
italian: "ita",
|
|
portuguese: "por",
|
|
japanese: "jpn",
|
|
korean: "kor",
|
|
chinese: "zho",
|
|
mandarin: "zho",
|
|
cantonese: "zho",
|
|
arabic: "ara",
|
|
russian: "rus",
|
|
dutch: "nld",
|
|
flemish: "nld",
|
|
swedish: "swe",
|
|
norwegian: "nor",
|
|
danish: "dan",
|
|
finnish: "fin",
|
|
polish: "pol",
|
|
turkish: "tur",
|
|
thai: "tha",
|
|
hindi: "hin",
|
|
hungarian: "hun",
|
|
czech: "ces",
|
|
romanian: "ron",
|
|
greek: "ell",
|
|
hebrew: "heb",
|
|
persian: "fas",
|
|
farsi: "fas",
|
|
ukrainian: "ukr",
|
|
indonesian: "ind",
|
|
malay: "msa",
|
|
vietnamese: "vie",
|
|
catalan: "cat",
|
|
tamil: "tam",
|
|
telugu: "tel",
|
|
icelandic: "isl",
|
|
croatian: "hrv",
|
|
bulgarian: "bul",
|
|
serbian: "srp",
|
|
slovak: "slk",
|
|
slovenian: "slv",
|
|
latvian: "lav",
|
|
lithuanian: "lit",
|
|
estonian: "est",
|
|
};
|
|
|
|
function nameToIso(name: string): string | null {
|
|
const key = name.toLowerCase().trim();
|
|
if (NAME_TO_639_2[key]) return NAME_TO_639_2[key];
|
|
if (key.length === 2 || key.length === 3) return normalizeLanguage(key);
|
|
warn(`Sonarr language name not recognised: '${name}'.`);
|
|
return null;
|
|
}
|