/** * Thin Radarr v3 API client. Used to add new movies, list quality * profiles and root folders, and reconcile our TMDB-keyed view of the * library against what Radarr already has or is downloading. * * All endpoints expect an `X-Api-Key` header. Calls return null on * network or HTTP failure so callers can fall through gracefully. */ import type { ArrInstance } from '../stores/arr-instances-store' export interface RadarrRootFolder { id: number path: string freeSpace?: number unmappedFolders?: { name: string; path: string }[] } export interface RadarrQualityProfile { id: number name: string cutoff: number } export interface RadarrTag { id: number label: string } export interface RadarrMovie { id?: number title: string originalTitle?: string year?: number tmdbId: number imdbId?: string | null hasFile?: boolean monitored?: boolean status?: 'announced' | 'inCinemas' | 'released' | 'deleted' qualityProfileId?: number rootFolderPath?: string tags?: number[] added?: string sizeOnDisk?: number isAvailable?: boolean /** Posters / fanart attached. */ images?: { coverType: string; remoteUrl?: string; url?: string }[] } export interface RadarrLookupResult extends RadarrMovie { /** Returned by /lookup with no `id` until added. */ folder?: string } export interface RadarrQueueItem { id: number movieId?: number title: string status: string trackedDownloadStatus?: string size?: number sizeleft?: number timeleft?: string estimatedCompletionTime?: string } export interface RadarrSystemStatus { version: string startTime?: string branch?: string appData?: string isProduction?: boolean } export interface AddRadarrMoviePayload { tmdbId: number qualityProfileId: number rootFolderPath: string monitored?: boolean searchOnAdd?: boolean tags?: number[] /** Required by Radarr but the lookup payload supplies it; we pass it back unchanged. */ title: string titleSlug: string year: number images?: { coverType: string; remoteUrl?: string; url?: string }[] minimumAvailability?: 'announced' | 'inCinemas' | 'released' | 'preDB' } export function buildAddRadarrMovieBody(payload: AddRadarrMoviePayload) { return { tmdbId: payload.tmdbId, qualityProfileId: payload.qualityProfileId, rootFolderPath: payload.rootFolderPath, monitored: payload.monitored ?? true, title: payload.title, titleSlug: payload.titleSlug, year: payload.year, images: payload.images || [], tags: payload.tags || [], minimumAvailability: payload.minimumAvailability || 'released', addOptions: { searchForMovie: payload.searchOnAdd ?? true, monitor: 'movieOnly', }, } } class RadarrClient { instance: ArrInstance constructor(instance: ArrInstance) { this.instance = instance } private url(path: string): string { const base = this.instance.baseUrl.replace(/\/+$/, '') return `${base}/api/v3${path}` } private async req(path: string, init: RequestInit = {}): Promise { try { const res = await fetch(this.url(path), { ...init, headers: { 'X-Api-Key': this.instance.apiKey, 'Content-Type': 'application/json', ...(init.headers || {}), }, }) if (!res.ok) return null const text = await res.text() return text ? (JSON.parse(text) as T) : (null as unknown as T) } catch { return null } } systemStatus() { return this.req('/system/status') } qualityProfiles() { return this.req('/qualityprofile') } rootFolders() { return this.req('/rootfolder') } tags() { return this.req('/tag') } movies() { return this.req('/movie') } queue() { return this.req<{ records: RadarrQueueItem[] }>('/queue') } /** * Look up a movie by TMDB id. Returns the metadata Radarr needs to add * it; `id` is undefined until the movie has been added. */ lookupByTmdbId(tmdbId: number) { return this.req( `/movie/lookup/tmdb?tmdbId=${encodeURIComponent(tmdbId)}`, ) } async addMovie(payload: AddRadarrMoviePayload): Promise { const body = buildAddRadarrMovieBody(payload) return this.req('/movie', { method: 'POST', body: JSON.stringify(body), }) } removeMovie(id: number, deleteFiles = false) { return this.req( `/movie/${id}?deleteFiles=${deleteFiles ? 'true' : 'false'}&addImportExclusion=false`, { method: 'DELETE' }, ) } searchMovie(id: number) { return this.req('/command', { method: 'POST', body: JSON.stringify({ name: 'MoviesSearch', movieIds: [id] }), }) } } export function radarrClient(instance: ArrInstance) { return new RadarrClient(instance) }