api clients for jellyfin, tmdb, fanart, etc.

This commit is contained in:
2026-03-23 13:32:13 +02:00
parent d65da148d4
commit 292b3f42cf
14 changed files with 2903 additions and 0 deletions
+180
View File
@@ -0,0 +1,180 @@
/**
* 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<T>(path: string, init: RequestInit = {}): Promise<T | null> {
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<RadarrSystemStatus>('/system/status') }
qualityProfiles() { return this.req<RadarrQualityProfile[]>('/qualityprofile') }
rootFolders() { return this.req<RadarrRootFolder[]>('/rootfolder') }
tags() { return this.req<RadarrTag[]>('/tag') }
movies() { return this.req<RadarrMovie[]>('/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<RadarrLookupResult[]>(
`/movie/lookup/tmdb?tmdbId=${encodeURIComponent(tmdbId)}`,
)
}
async addMovie(payload: AddRadarrMoviePayload): Promise<RadarrMovie | null> {
const body = buildAddRadarrMovieBody(payload)
return this.req<RadarrMovie>('/movie', {
method: 'POST',
body: JSON.stringify(body),
})
}
removeMovie(id: number, deleteFiles = false) {
return this.req<void>(
`/movie/${id}?deleteFiles=${deleteFiles ? 'true' : 'false'}&addImportExclusion=false`,
{ method: 'DELETE' },
)
}
searchMovie(id: number) {
return this.req<void>('/command', {
method: 'POST',
body: JSON.stringify({ name: 'MoviesSearch', movieIds: [id] }),
})
}
}
export function radarrClient(instance: ArrInstance) {
return new RadarrClient(instance)
}