// Package radarr is a client for the Radarr v3 API. package radarr import ( "context" "encoding/json" "net/http" "net/url" "strconv" "git.stinnesbeck.com/nils/arr-tui/internal/arr" ) // Client talks to a Radarr instance. The embedded arr.Client provides // SystemStatus, QualityProfiles and RootFolders. type Client struct { *arr.Client } // NewClient returns a Radarr client. func NewClient(baseURL, apiKey string) *Client { return &Client{arr.NewClient(baseURL, apiKey)} } // Ratings holds the per-source ratings Radarr reports. type Ratings struct { IMDB *Rating `json:"imdb,omitempty"` TMDB *Rating `json:"tmdb,omitempty"` Metacritic *Rating `json:"metacritic,omitempty"` RottenTomatoes *Rating `json:"rottenTomatoes,omitempty"` } // Rating is a single rating value. type Rating struct { Votes int `json:"votes"` Value float64 `json:"value"` } // Movie is a lookup result or library movie. ID is 0 when the movie is not // in the library yet. type Movie struct { ID int `json:"id"` Title string `json:"title"` OriginalTitle string `json:"originalTitle,omitempty"` Year int `json:"year"` Overview string `json:"overview,omitempty"` Runtime int `json:"runtime"` Genres []string `json:"genres,omitempty"` Studio string `json:"studio,omitempty"` Status string `json:"status,omitempty"` TMDBID int `json:"tmdbId"` IMDBID string `json:"imdbId,omitempty"` TitleSlug string `json:"titleSlug,omitempty"` Images []arr.Image `json:"images,omitempty"` Ratings Ratings `json:"ratings"` Monitored bool `json:"monitored"` HasFile bool `json:"hasFile"` QualityProfileID int `json:"qualityProfileId"` SizeOnDisk int64 `json:"sizeOnDisk"` IsAvailable bool `json:"isAvailable"` Path string `json:"path,omitempty"` MovieFile *arr.MediaFile `json:"movieFile,omitempty"` } // QualityName returns the quality of the downloaded file, or "". func (m Movie) QualityName() string { if m.MovieFile == nil { return "" } return m.MovieFile.Quality.Quality.Name } // InLibrary reports whether the movie has already been added to Radarr. func (m Movie) InLibrary() bool { return m.ID != 0 } // Rating returns the best available rating (IMDb, then TMDb), or 0. func (m Movie) Rating() float64 { switch { case m.Ratings.IMDB != nil && m.Ratings.IMDB.Value > 0: return m.Ratings.IMDB.Value case m.Ratings.TMDB != nil: return m.Ratings.TMDB.Value } return 0 } // Lookup searches for movies by free text ("The Matrix"), "tmdb:603" or "imdb:tt0133093". func (c *Client) Lookup(ctx context.Context, term string) ([]Movie, error) { var movies []Movie err := c.Do(ctx, http.MethodGet, "movie/lookup", url.Values{"term": {term}}, nil, &movies) return movies, err } // Movies lists the movies in the library. func (c *Client) Movies(ctx context.Context) ([]Movie, error) { var movies []Movie err := c.Do(ctx, http.MethodGet, "movie", nil, nil, &movies) return movies, err } // AddOptions are the choices from the add form. type AddOptions struct { QualityProfileID int RootFolderPath string Monitored bool SearchNow bool MinimumAvailability string // "announced", "inCinemas" or "released"; defaults to "released" } type addMovieRequest struct { Title string `json:"title"` Year int `json:"year"` TMDBID int `json:"tmdbId"` TitleSlug string `json:"titleSlug,omitempty"` Images []arr.Image `json:"images,omitempty"` QualityProfileID int `json:"qualityProfileId"` RootFolderPath string `json:"rootFolderPath"` Monitored bool `json:"monitored"` MinimumAvailability string `json:"minimumAvailability"` AddOptions struct { SearchForMovie bool `json:"searchForMovie"` } `json:"addOptions"` } // AddMovie adds a lookup result to Radarr and returns the created movie. func (c *Client) AddMovie(ctx context.Context, m Movie, opts AddOptions) (Movie, error) { req := addMovieRequest{ Title: m.Title, Year: m.Year, TMDBID: m.TMDBID, TitleSlug: m.TitleSlug, Images: m.Images, QualityProfileID: opts.QualityProfileID, RootFolderPath: opts.RootFolderPath, Monitored: opts.Monitored, MinimumAvailability: opts.MinimumAvailability, } if req.MinimumAvailability == "" { req.MinimumAvailability = "released" } req.AddOptions.SearchForMovie = opts.SearchNow var created Movie err := c.Do(ctx, http.MethodPost, "movie", nil, req, &created) return created, err } // SearchMovie triggers an automatic search for an existing library movie. func (c *Client) SearchMovie(ctx context.Context, movieID int) error { return c.Command(ctx, map[string]any{"name": "MoviesSearch", "movieIds": []int{movieID}}) } // TMDBTerm builds a lookup term for a TMDb id. func TMDBTerm(tmdbID int) string { return "tmdb:" + strconv.Itoa(tmdbID) } // Queue lists Radarr's download queue, including movie info. func (c *Client) Queue(ctx context.Context) ([]arr.QueueItem, error) { return c.QueueWith(ctx, url.Values{"includeMovie": {"true"}}) } // Releases runs an interactive search for a library movie. func (c *Client) Releases(ctx context.Context, movieID int) ([]arr.Release, error) { return c.Client.Releases(ctx, url.Values{"movieId": {strconv.Itoa(movieID)}}) } // SetMonitored changes the monitored flag of a library movie. func (c *Client) SetMonitored(ctx context.Context, movieID int, monitored bool) error { return c.Update(ctx, "movie", movieID, func(o map[string]json.RawMessage) error { arr.SetBool(o, "monitored", monitored) return nil }) } // DeleteMovie removes a movie from the library. With deleteFiles the movie's // files are deleted from disk too; addExclusion keeps it from coming back // through import lists. func (c *Client) DeleteMovie(ctx context.Context, id int, deleteFiles, addExclusion bool) error { q := url.Values{ "deleteFiles": {strconv.FormatBool(deleteFiles)}, "addImportExclusion": {strconv.FormatBool(addExclusion)}, } return c.Do(ctx, http.MethodDelete, "movie/"+strconv.Itoa(id), q, nil, nil) }