restructure to react spa + hono api, fix missing server/ and lib/
rewrite from monolithic hono jsx to react 19 spa with tanstack router + hono json api backend. add scan, review, execute, nodes, and setup pages. multi-stage dockerfile (node for vite build, bun for runtime). previously, server/ and src/shared/lib/ were silently excluded by global gitignore patterns (/server/ from emacs, lib/ from python). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
108
server/services/radarr.ts
Normal file
108
server/services/radarr.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user