scan: validate arr URLs upfront, cache library once per scan
All checks were successful
Build and Push Docker Image / build (push) Successful in 30s
All checks were successful
Build and Push Docker Image / build (push) Successful in 30s
Two regressions from the radarr/sonarr fix: 1. ERR_INVALID_URL spam — when radarr_enabled='1' but radarr_url is empty or not http(s), every per-item fetch threw TypeError. We caught it but still ate the cost (and the log noise) on every movie. New isUsable() check on each service: enabled-in-config but URL doesn't parse → warn ONCE and skip arr lookups for the whole scan. 2. Per-item HTTP storm — for movies not in Radarr's library we used to hit /api/v3/movie (the WHOLE library) again per item, then two metadata-lookup calls. With 2000 items that's thousands of extra round-trips and the scan crawled. Now: pre-load the Radarr/Sonarr library once into Map<tmdbId,..>+Map<imdbId,..>+Map<tvdbId,..>, per-item lookups are O(1) memory hits, and only the genuinely-missing items make a single lookup-endpoint HTTP call. The startup line now reports the library size: External language sources: radarr=enabled (https://..., 1287 movies in library), sonarr=... so you can immediately see whether the cache loaded.
This commit is contained in:
@@ -4,8 +4,16 @@ import { getAllConfig, getConfig, getDb, setConfig } from "../db/index";
|
||||
import { log, error as logError, warn } from "../lib/log";
|
||||
import { analyzeItem } from "../services/analyzer";
|
||||
import { extractOriginalLanguage, getAllItems, getDevItems, mapStream, normalizeLanguage } from "../services/jellyfin";
|
||||
import { getOriginalLanguage as radarrLang } from "../services/radarr";
|
||||
import { getOriginalLanguage as sonarrLang } from "../services/sonarr";
|
||||
import {
|
||||
loadLibrary as loadRadarrLibrary,
|
||||
getOriginalLanguage as radarrLang,
|
||||
isUsable as radarrUsable,
|
||||
} from "../services/radarr";
|
||||
import {
|
||||
loadLibrary as loadSonarrLibrary,
|
||||
getOriginalLanguage as sonarrLang,
|
||||
isUsable as sonarrUsable,
|
||||
} from "../services/sonarr";
|
||||
import type { MediaStream } from "../types";
|
||||
|
||||
const app = new Hono();
|
||||
@@ -152,13 +160,31 @@ async function runScan(limit: number | null = null): Promise<void> {
|
||||
const jellyfinCfg = { url: cfg.jellyfin_url, apiKey: cfg.jellyfin_api_key, userId: cfg.jellyfin_user_id };
|
||||
const subtitleLanguages = parseLanguageList(cfg.subtitle_languages ?? null, ["eng", "deu", "spa"]);
|
||||
const audioLanguages = parseLanguageList(cfg.audio_languages ?? null, []);
|
||||
const radarrEnabled = cfg.radarr_enabled === "1";
|
||||
const sonarrEnabled = cfg.sonarr_enabled === "1";
|
||||
const radarrCfg = { url: cfg.radarr_url, apiKey: cfg.radarr_api_key };
|
||||
const sonarrCfg = { url: cfg.sonarr_url, apiKey: cfg.sonarr_api_key };
|
||||
// 'enabled' in config means the user toggled it on. Only actually use it
|
||||
// if the URL+key pass URL parsing — otherwise we'd hit ERR_INVALID_URL on
|
||||
// every item. Refuse to call invalid endpoints rather than spamming logs.
|
||||
const radarrEnabled = cfg.radarr_enabled === "1" && radarrUsable(radarrCfg);
|
||||
const sonarrEnabled = cfg.sonarr_enabled === "1" && sonarrUsable(sonarrCfg);
|
||||
|
||||
if (cfg.radarr_enabled === "1" && !radarrEnabled) {
|
||||
warn(`Radarr is enabled in config but URL/API key is invalid (url='${cfg.radarr_url}') — skipping Radarr lookups`);
|
||||
}
|
||||
if (cfg.sonarr_enabled === "1" && !sonarrEnabled) {
|
||||
warn(`Sonarr is enabled in config but URL/API key is invalid (url='${cfg.sonarr_url}') — skipping Sonarr lookups`);
|
||||
}
|
||||
|
||||
// Pre-load both libraries once so per-item lookups are O(1) cache hits
|
||||
// instead of HTTP round-trips. The previous code called /api/v3/movie
|
||||
// (the entire library!) once per item that didn't match by tmdbId.
|
||||
const [radarrLibrary, sonarrLibrary] = await Promise.all([
|
||||
radarrEnabled ? loadRadarrLibrary(radarrCfg) : Promise.resolve(null),
|
||||
sonarrEnabled ? loadSonarrLibrary(sonarrCfg) : Promise.resolve(null),
|
||||
]);
|
||||
|
||||
// Log the external-source situation once per scan so it's obvious from
|
||||
// logs whether Radarr/Sonarr are actually being consulted.
|
||||
log(
|
||||
`External language sources: radarr=${radarrEnabled ? `enabled (${cfg.radarr_url || "NO URL"})` : "disabled"}, sonarr=${sonarrEnabled ? `enabled (${cfg.sonarr_url || "NO URL"})` : "disabled"}`,
|
||||
`External language sources: radarr=${radarrEnabled ? `enabled (${cfg.radarr_url}, ${radarrLibrary?.byTmdbId.size ?? 0} movies in library)` : "disabled"}, sonarr=${sonarrEnabled ? `enabled (${cfg.sonarr_url}, ${sonarrLibrary?.byTvdbId.size ?? 0} series in library)` : "disabled"}`,
|
||||
);
|
||||
let radarrMisses = 0;
|
||||
let radarrHits = 0;
|
||||
@@ -250,14 +276,15 @@ async function runScan(limit: number | null = null): Promise<void> {
|
||||
let needsReview = origLang ? 0 : 1;
|
||||
let authoritative = false; // set when Radarr/Sonarr answers
|
||||
|
||||
if (jellyfinItem.Type === "Movie" && radarrEnabled) {
|
||||
if (jellyfinItem.Type === "Movie" && radarrEnabled && radarrLibrary) {
|
||||
if (!tmdbId && !imdbId) {
|
||||
missingProviderIds++;
|
||||
warn(`No tmdb/imdb id on '${jellyfinItem.Name}' — Radarr lookup skipped`);
|
||||
} else {
|
||||
const lang = await radarrLang(
|
||||
{ url: cfg.radarr_url, apiKey: cfg.radarr_api_key },
|
||||
radarrCfg,
|
||||
{ tmdbId: tmdbId ?? undefined, imdbId: imdbId ?? undefined },
|
||||
radarrLibrary,
|
||||
);
|
||||
if (lang) {
|
||||
radarrHits++;
|
||||
@@ -274,12 +301,12 @@ async function runScan(limit: number | null = null): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
if (jellyfinItem.Type === "Episode" && sonarrEnabled) {
|
||||
if (jellyfinItem.Type === "Episode" && sonarrEnabled && sonarrLibrary) {
|
||||
if (!tvdbId) {
|
||||
missingProviderIds++;
|
||||
warn(`No tvdb id on '${jellyfinItem.Name}' — Sonarr lookup skipped`);
|
||||
} else {
|
||||
const lang = await sonarrLang({ url: cfg.sonarr_url, apiKey: cfg.sonarr_api_key }, tvdbId);
|
||||
const lang = await sonarrLang(sonarrCfg, tvdbId, sonarrLibrary);
|
||||
if (lang) {
|
||||
sonarrHits++;
|
||||
if (origLang && normalizeLanguage(origLang) !== normalizeLanguage(lang)) needsReview = 1;
|
||||
|
||||
Reference in New Issue
Block a user