api clients for jellyfin, tmdb, fanart, etc.
This commit is contained in:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user