Files
nabarr/radarr/api.go
2026-01-03 11:07:48 +01:00

254 lines
6.4 KiB
Go

package radarr
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"strconv"
"strings"
"github.com/lucperkins/rek"
"github.com/l3uddz/nabarr"
"github.com/l3uddz/nabarr/media"
"github.com/l3uddz/nabarr/util"
)
var (
ErrItemNotFound = errors.New("not found")
)
func (c *Client) getSystemStatus() (*systemStatus, error) {
// send request
resp, err := rek.Get(util.JoinURL(c.apiURL, "system", "status"), rek.Client(c.http), rek.Headers(c.apiHeaders))
if err != nil {
return nil, fmt.Errorf("request system status: %w", err)
}
defer resp.Body().Close()
// validate response
if resp.StatusCode() != 200 {
return nil, fmt.Errorf("validate system status response: %s", resp.Status())
}
// decode response
b := new(systemStatus)
if err := json.NewDecoder(resp.Body()).Decode(b); err != nil {
return nil, fmt.Errorf("decode system status response: %w", err)
}
return b, nil
}
func (c *Client) getQualityProfileId(profileName string) (int, error) {
// send request
resp, err := rek.Get(util.JoinURL(c.apiURL, "qualityprofile"), rek.Client(c.http), rek.Headers(c.apiHeaders))
if err != nil {
return 0, fmt.Errorf("request quality profiles: %w", err)
}
defer resp.Body().Close()
// validate response
if resp.StatusCode() != 200 {
return 0, fmt.Errorf("validate quality profiles response: %s", resp.Status())
}
// decode response
b := new([]qualityProfile)
if err := json.NewDecoder(resp.Body()).Decode(b); err != nil {
return 0, fmt.Errorf("decode quality profiles response: %w", err)
}
// find quality profile
for _, profile := range *b {
if strings.EqualFold(profile.Name, profileName) {
return profile.Id, nil
}
}
return 0, errors.New("quality profile not found")
}
func (c *Client) lookupMediaItem(item *media.Item) (*lookupRequest, error) {
// retrieve and validate media provider data
mdp, mdi := item.GetProviderData()
if mdp == "" || mdi == "" {
return nil, fmt.Errorf("no media provider details found")
}
// prepare request
reqUrl, err := util.URLWithQuery(util.JoinURL(c.apiURL, "movie", "lookup"),
url.Values{"term": []string{fmt.Sprintf("%s:%s", mdp, mdi)}})
if err != nil {
return nil, fmt.Errorf("generate movie lookup request url: %w", err)
}
// send request
resp, err := rek.Get(reqUrl, rek.Client(c.http), rek.Headers(c.apiHeaders))
if err != nil {
return nil, fmt.Errorf("request movie lookup: %w", err)
}
defer resp.Body().Close()
// validate response
if resp.StatusCode() != 200 {
return nil, fmt.Errorf("validate movie lookup response: %s", resp.Status())
}
// decode response
b := new([]lookupRequest)
if err := json.NewDecoder(resp.Body()).Decode(b); err != nil {
return nil, fmt.Errorf("decode movie lookup response: %w", err)
}
// find movie
for _, s := range *b {
switch mdp {
case "tmdb":
if strconv.Itoa(s.TmdbId) == mdi {
return &s, nil
}
default:
if s.ImdbId == mdi {
return &s, nil
}
}
}
return nil, fmt.Errorf("movie lookup %sId: %v: %w", mdp, mdi, ErrItemNotFound)
}
func (c *Client) getExclusions() (map[int]exclusion, error) {
// send request
resp, err := rek.Get(util.JoinURL(c.apiURL, "exclusions"), rek.Client(c.http), rek.Headers(c.apiHeaders))
if err != nil {
return nil, fmt.Errorf("request exclusions: %w", err)
}
defer resp.Body().Close()
// validate response
if resp.StatusCode() != 200 {
return nil, fmt.Errorf("validate exclusions response: %s", resp.Status())
}
// decode response
b := new([]exclusion)
if err := json.NewDecoder(resp.Body()).Decode(b); err != nil {
return nil, fmt.Errorf("decode exclusions response: %w", err)
}
// generate exclusion map
exclusions := make(map[int]exclusion)
for n := range *b {
e := (*b)[n]
exclusions[e.TmdbId] = e
}
return exclusions, nil
}
func (c *Client) getOrCreateTag(tagName string) (int, error) {
// get all tags
resp, err := rek.Get(util.JoinURL(c.apiURL, "tag"), rek.Client(c.http), rek.Headers(c.apiHeaders))
if err != nil {
return 0, fmt.Errorf("request tags: %w", err)
}
defer resp.Body().Close()
// validate response
if resp.StatusCode() != 200 {
return 0, fmt.Errorf("validate tags response: %s", resp.Status())
}
// decode response
tags := new([]tag)
if err := json.NewDecoder(resp.Body()).Decode(tags); err != nil {
return 0, fmt.Errorf("decode tags response: %w", err)
}
// find tag by name
for _, t := range *tags {
if strings.EqualFold(t.Label, tagName) {
return t.Id, nil
}
}
// tag not found, create it
createReq := tag{
Label: tagName,
}
createResp, err := rek.Post(util.JoinURL(c.apiURL, "tag"), rek.Client(c.http), rek.Headers(c.apiHeaders),
rek.Json(createReq))
if err != nil {
return 0, fmt.Errorf("request create tag: %w", err)
}
defer createResp.Body().Close()
// validate response
if createResp.StatusCode() != 200 && createResp.StatusCode() != 201 {
return 0, fmt.Errorf("validate create tag response: %s", createResp.Status())
}
// decode created tag
createdTag := new(tag)
if err := json.NewDecoder(createResp.Body()).Decode(createdTag); err != nil {
return 0, fmt.Errorf("decode create tag response: %w", err)
}
return createdTag.Id, nil
}
func (c *Client) AddMediaItem(item *media.Item, opts ...nabarr.PvrOption) error {
// prepare options
o, err := nabarr.BuildPvrOptions(opts...)
if err != nil {
return fmt.Errorf("build options: %v: %w", item.TmdbId, err)
}
// prepare tags
tags := []int{}
if o.Tag != "" {
tagId, err := c.getOrCreateTag(o.Tag)
if err != nil {
return fmt.Errorf("get or create tag %q: %w", o.Tag, err)
}
tags = []int{tagId}
}
// prepare request
req := addRequest{
Title: item.Title,
TitleSlug: item.Slug,
Year: item.Year,
QualityProfileId: c.qualityProfileId,
Images: []string{},
Tags: tags,
Monitored: o.AddMonitored,
RootFolderPath: c.rootFolder,
MinimumAvailability: "released",
AddOptions: addOptions{
SearchForMovie: o.SearchMissing,
IgnoreEpisodesWithFiles: false,
IgnoreEpisodesWithoutFiles: false,
},
TmdbId: util.Atoi(item.TmdbId, 0),
ImdbId: item.ImdbId,
}
// send request
resp, err := rek.Post(util.JoinURL(c.apiURL, "movie"), rek.Client(c.http), rek.Headers(c.apiHeaders),
rek.Json(req))
if err != nil {
return fmt.Errorf("request add movie: %w", err)
}
defer resp.Body().Close()
// validate response
if resp.StatusCode() != 200 && resp.StatusCode() != 201 {
return fmt.Errorf("validate add movie response: %s", resp.Status())
}
return nil
}