Files
arr-tui/internal/radarr/radarr.go
T
nilsandClaude Sonnet 5.5 446cbdd6c5 Show Radarr and Sonarr queues in the Downloads view (M5)
Add queue listing and removal/blocklisting to the shared arr client.
The Downloads view now fetches SABnzbd and both arr queues in parallel,
shows status, progress and import problems, flags items needing
attention in the summary, and supports removing or blocklisting items.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 18:03:16 +02:00

148 lines
5.0 KiB
Go

// Package radarr is a client for the Radarr v3 API.
package radarr
import (
"context"
"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"`
}
// 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 {
body := map[string]any{"name": "MoviesSearch", "movieIds": []int{movieID}}
return c.Do(ctx, http.MethodPost, "command", nil, body, nil)
}
// 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"}})
}