181 lines
4.8 KiB
TypeScript
181 lines
4.8 KiB
TypeScript
/**
|
|
* 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)
|
|
}
|