- execute: actually call isInScheduleWindow/waitForWindow/sleepBetweenJobs in runSequential (they were dead code); emit queue_status SSE events (running/paused/sleeping/idle) so the pipeline's existing QueueStatus listener lights up - review: POST /:id/retry resets an errored plan to approved, wipes old done/error jobs, rebuilds command from current decisions, queues fresh job - scan: dev-mode DELETE now also wipes jobs + subtitle_files (previously orphaned after every dev reset) - biome: migrate config to 2.4 schema, autoformat 68 files (strings + indentation), relax opinionated a11y/hooks-deps/index-key rules that don't fit this codebase - routeTree.gen.ts regenerated after /nodes removal
109 lines
2.5 KiB
TypeScript
109 lines
2.5 KiB
TypeScript
import { normalizeLanguage } from "./jellyfin";
|
|
|
|
export interface RadarrConfig {
|
|
url: string;
|
|
apiKey: string;
|
|
}
|
|
|
|
function headers(apiKey: string): Record<string, string> {
|
|
return { "X-Api-Key": apiKey };
|
|
}
|
|
|
|
export async function testConnection(cfg: RadarrConfig): 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 RadarrMovie {
|
|
tmdbId?: number;
|
|
imdbId?: string;
|
|
originalLanguage?: { name: string; nameTranslated?: string };
|
|
}
|
|
|
|
/** Returns ISO 639-2 original language or null. */
|
|
export async function getOriginalLanguage(
|
|
cfg: RadarrConfig,
|
|
ids: { tmdbId?: string; imdbId?: string },
|
|
): Promise<string | null> {
|
|
try {
|
|
let movie: RadarrMovie | null = null;
|
|
|
|
if (ids.tmdbId) {
|
|
const res = await fetch(`${cfg.url}/api/v3/movie?tmdbId=${ids.tmdbId}`, {
|
|
headers: headers(cfg.apiKey),
|
|
});
|
|
if (res.ok) {
|
|
const list = (await res.json()) as RadarrMovie[];
|
|
movie = list[0] ?? null;
|
|
}
|
|
}
|
|
|
|
if (!movie && ids.imdbId) {
|
|
const res = await fetch(`${cfg.url}/api/v3/movie`, {
|
|
headers: headers(cfg.apiKey),
|
|
});
|
|
if (res.ok) {
|
|
const list = (await res.json()) as (RadarrMovie & { imdbId?: string })[];
|
|
movie = list.find((m) => m.imdbId === ids.imdbId) ?? null;
|
|
}
|
|
}
|
|
|
|
if (!movie?.originalLanguage) return null;
|
|
return iso6391To6392(movie.originalLanguage.name) ?? null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
// Radarr returns language names like "English", "French", "German", etc.
|
|
// Map them to ISO 639-2 codes.
|
|
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",
|
|
arabic: "ara",
|
|
russian: "rus",
|
|
dutch: "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",
|
|
ukrainian: "ukr",
|
|
indonesian: "ind",
|
|
malay: "msa",
|
|
vietnamese: "vie",
|
|
catalan: "cat",
|
|
tamil: "tam",
|
|
telugu: "tel",
|
|
"brazilian portuguese": "por",
|
|
"portuguese (brazil)": "por",
|
|
};
|
|
|
|
function iso6391To6392(name: string): string | null {
|
|
const key = name.toLowerCase().trim();
|
|
return NAME_TO_639_2[key] ?? normalizeLanguage(key.slice(0, 3)) ?? null;
|
|
}
|