278 lines
10 KiB
Go
278 lines
10 KiB
Go
// Package sonarr is a client for the Sonarr v3/v4 API.
|
|
package sonarr
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"time"
|
|
|
|
"git.stinnesbeck.com/nils/arr-tui/internal/arr"
|
|
)
|
|
|
|
// Client talks to a Sonarr instance. The embedded arr.Client provides
|
|
// SystemStatus, QualityProfiles and RootFolders.
|
|
type Client struct {
|
|
*arr.Client
|
|
}
|
|
|
|
// NewClient returns a Sonarr client.
|
|
func NewClient(baseURL, apiKey string) *Client {
|
|
return &Client{arr.NewClient(baseURL, apiKey)}
|
|
}
|
|
|
|
// Monitor modes accepted by addOptions.monitor.
|
|
const (
|
|
MonitorAll = "all"
|
|
MonitorFuture = "future"
|
|
MonitorMissing = "missing"
|
|
MonitorExisting = "existing"
|
|
MonitorFirstSeason = "firstSeason"
|
|
MonitorLatestSeason = "latestSeason"
|
|
MonitorPilot = "pilot"
|
|
MonitorNone = "none"
|
|
)
|
|
|
|
// Rating is a series rating.
|
|
type Rating struct {
|
|
Votes int `json:"votes"`
|
|
Value float64 `json:"value"`
|
|
}
|
|
|
|
// Season is a season entry of a series.
|
|
type Season struct {
|
|
SeasonNumber int `json:"seasonNumber"`
|
|
Monitored bool `json:"monitored"`
|
|
Statistics Statistics `json:"statistics"`
|
|
}
|
|
|
|
// Statistics are the episode counts Sonarr reports for library series.
|
|
type Statistics struct {
|
|
SeasonCount int `json:"seasonCount"`
|
|
EpisodeFileCount int `json:"episodeFileCount"`
|
|
EpisodeCount int `json:"episodeCount"`
|
|
TotalEpisodeCount int `json:"totalEpisodeCount"`
|
|
SizeOnDisk int64 `json:"sizeOnDisk"`
|
|
}
|
|
|
|
// Series is a lookup result or library series. ID is 0 when the series is
|
|
// not in the library yet.
|
|
type Series struct {
|
|
ID int `json:"id"`
|
|
Title string `json:"title"`
|
|
Year int `json:"year"`
|
|
Overview string `json:"overview,omitempty"`
|
|
Network string `json:"network,omitempty"`
|
|
Status string `json:"status,omitempty"`
|
|
Runtime int `json:"runtime"`
|
|
Genres []string `json:"genres,omitempty"`
|
|
TVDBID int `json:"tvdbId"`
|
|
TMDBID int `json:"tmdbId,omitempty"`
|
|
IMDBID string `json:"imdbId,omitempty"`
|
|
TitleSlug string `json:"titleSlug,omitempty"`
|
|
SeriesType string `json:"seriesType,omitempty"`
|
|
Images []arr.Image `json:"images,omitempty"`
|
|
Ratings Rating `json:"ratings"`
|
|
Seasons []Season `json:"seasons,omitempty"`
|
|
Monitored bool `json:"monitored"`
|
|
Statistics Statistics `json:"statistics"`
|
|
}
|
|
|
|
// InLibrary reports whether the series has already been added to Sonarr.
|
|
func (s Series) InLibrary() bool { return s.ID != 0 }
|
|
|
|
// SeasonCount returns the number of regular seasons (excluding specials).
|
|
func (s Series) SeasonCount() int {
|
|
n := 0
|
|
for _, season := range s.Seasons {
|
|
if season.SeasonNumber > 0 {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
// Lookup searches for series by free text, "tvdb:81189" or "imdb:tt0903747".
|
|
func (c *Client) Lookup(ctx context.Context, term string) ([]Series, error) {
|
|
var series []Series
|
|
err := c.Do(ctx, http.MethodGet, "series/lookup", url.Values{"term": {term}}, nil, &series)
|
|
return series, err
|
|
}
|
|
|
|
// Series lists the series in the library.
|
|
func (c *Client) Series(ctx context.Context) ([]Series, error) {
|
|
var series []Series
|
|
err := c.Do(ctx, http.MethodGet, "series", nil, nil, &series)
|
|
return series, err
|
|
}
|
|
|
|
// AddOptions are the choices from the add form.
|
|
type AddOptions struct {
|
|
QualityProfileID int
|
|
RootFolderPath string
|
|
Monitored bool
|
|
Monitor string // one of the Monitor* constants; defaults to MonitorAll
|
|
SearchNow bool // search for missing episodes after adding
|
|
}
|
|
|
|
type addSeriesRequest struct {
|
|
Title string `json:"title"`
|
|
Year int `json:"year"`
|
|
TVDBID int `json:"tvdbId"`
|
|
TitleSlug string `json:"titleSlug,omitempty"`
|
|
SeriesType string `json:"seriesType,omitempty"`
|
|
Images []arr.Image `json:"images,omitempty"`
|
|
Seasons []Season `json:"seasons,omitempty"`
|
|
QualityProfileID int `json:"qualityProfileId"`
|
|
RootFolderPath string `json:"rootFolderPath"`
|
|
Monitored bool `json:"monitored"`
|
|
SeasonFolder bool `json:"seasonFolder"`
|
|
AddOptions struct {
|
|
Monitor string `json:"monitor"`
|
|
SearchForMissingEpisodes bool `json:"searchForMissingEpisodes"`
|
|
} `json:"addOptions"`
|
|
}
|
|
|
|
// AddSeries adds a lookup result to Sonarr and returns the created series.
|
|
func (c *Client) AddSeries(ctx context.Context, s Series, opts AddOptions) (Series, error) {
|
|
req := addSeriesRequest{
|
|
Title: s.Title,
|
|
Year: s.Year,
|
|
TVDBID: s.TVDBID,
|
|
TitleSlug: s.TitleSlug,
|
|
SeriesType: s.SeriesType,
|
|
Images: s.Images,
|
|
Seasons: s.Seasons,
|
|
QualityProfileID: opts.QualityProfileID,
|
|
RootFolderPath: opts.RootFolderPath,
|
|
Monitored: opts.Monitored,
|
|
SeasonFolder: true,
|
|
}
|
|
req.AddOptions.Monitor = opts.Monitor
|
|
if req.AddOptions.Monitor == "" {
|
|
req.AddOptions.Monitor = MonitorAll
|
|
}
|
|
req.AddOptions.SearchForMissingEpisodes = opts.SearchNow
|
|
|
|
var created Series
|
|
err := c.Do(ctx, http.MethodPost, "series", nil, req, &created)
|
|
return created, err
|
|
}
|
|
|
|
// SearchSeries triggers an automatic search for all missing episodes of a library series.
|
|
func (c *Client) SearchSeries(ctx context.Context, seriesID int) error {
|
|
return c.Command(ctx, map[string]any{"name": "SeriesSearch", "seriesId": seriesID})
|
|
}
|
|
|
|
// SearchSeason triggers an automatic search for the missing episodes of one season.
|
|
func (c *Client) SearchSeason(ctx context.Context, seriesID, season int) error {
|
|
return c.Command(ctx, map[string]any{"name": "SeasonSearch", "seriesId": seriesID, "seasonNumber": season})
|
|
}
|
|
|
|
// SearchEpisode triggers an automatic search for one episode.
|
|
func (c *Client) SearchEpisode(ctx context.Context, episodeID int) error {
|
|
return c.Command(ctx, map[string]any{"name": "EpisodeSearch", "episodeIds": []int{episodeID}})
|
|
}
|
|
|
|
// GetSeries fetches one library series.
|
|
func (c *Client) GetSeries(ctx context.Context, id int) (Series, error) {
|
|
var s Series
|
|
err := c.Do(ctx, http.MethodGet, "series/"+strconv.Itoa(id), nil, nil, &s)
|
|
return s, err
|
|
}
|
|
|
|
// SetMonitored changes the monitored flag of a library series.
|
|
func (c *Client) SetMonitored(ctx context.Context, seriesID int, monitored bool) error {
|
|
return c.Update(ctx, "series", seriesID, func(o map[string]json.RawMessage) error {
|
|
arr.SetBool(o, "monitored", monitored)
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// SetSeasonMonitored changes the monitored flag of one season of a library series.
|
|
func (c *Client) SetSeasonMonitored(ctx context.Context, seriesID, season int, monitored bool) error {
|
|
return c.Update(ctx, "series", seriesID, func(o map[string]json.RawMessage) error {
|
|
return arr.SetSeasonMonitored(o, season, monitored)
|
|
})
|
|
}
|
|
|
|
// SetEpisodesMonitored changes the monitored flag of episodes.
|
|
func (c *Client) SetEpisodesMonitored(ctx context.Context, episodeIDs []int, monitored bool) error {
|
|
body := map[string]any{"episodeIds": episodeIDs, "monitored": monitored}
|
|
return c.Do(ctx, http.MethodPut, "episode/monitor", nil, body, nil)
|
|
}
|
|
|
|
// Queue lists Sonarr's download queue, including series and episode info.
|
|
func (c *Client) Queue(ctx context.Context) ([]arr.QueueItem, error) {
|
|
return c.QueueWith(ctx, url.Values{"includeSeries": {"true"}, "includeEpisode": {"true"}})
|
|
}
|
|
|
|
// Episode is an episode of a library series.
|
|
type Episode struct {
|
|
ID int `json:"id"`
|
|
SeriesID int `json:"seriesId"`
|
|
SeasonNumber int `json:"seasonNumber"`
|
|
EpisodeNumber int `json:"episodeNumber"`
|
|
Title string `json:"title"`
|
|
AirDate string `json:"airDate"`
|
|
AirDateUTC string `json:"airDateUtc"`
|
|
Overview string `json:"overview,omitempty"`
|
|
HasFile bool `json:"hasFile"`
|
|
Monitored bool `json:"monitored"`
|
|
Series *Series `json:"series,omitempty"` // only with includeSeries
|
|
}
|
|
|
|
// Episodes lists all episodes of a library series.
|
|
func (c *Client) Episodes(ctx context.Context, seriesID int) ([]Episode, error) {
|
|
var episodes []Episode
|
|
err := c.Do(ctx, http.MethodGet, "episode", url.Values{"seriesId": {strconv.Itoa(seriesID)}}, nil, &episodes)
|
|
return episodes, err
|
|
}
|
|
|
|
// EpisodeReleases runs an interactive search for one episode.
|
|
func (c *Client) EpisodeReleases(ctx context.Context, episodeID int) ([]arr.Release, error) {
|
|
return c.Client.Releases(ctx, url.Values{"episodeId": {strconv.Itoa(episodeID)}})
|
|
}
|
|
|
|
// SeasonReleases runs an interactive search for a whole season (season packs and episodes).
|
|
func (c *Client) SeasonReleases(ctx context.Context, seriesID, season int) ([]arr.Release, error) {
|
|
return c.Client.Releases(ctx, url.Values{
|
|
"seriesId": {strconv.Itoa(seriesID)},
|
|
"seasonNumber": {strconv.Itoa(season)},
|
|
})
|
|
}
|
|
|
|
// EpisodeFiles lists the downloaded files of a library series.
|
|
func (c *Client) EpisodeFiles(ctx context.Context, seriesID int) ([]arr.MediaFile, error) {
|
|
var files []arr.MediaFile
|
|
err := c.Do(ctx, http.MethodGet, "episodefile", url.Values{"seriesId": {strconv.Itoa(seriesID)}}, nil, &files)
|
|
return files, err
|
|
}
|
|
|
|
// DeleteSeries removes a series from the library. With deleteFiles all its
|
|
// episode files are deleted from disk too; addExclusion keeps it from coming
|
|
// back through import lists.
|
|
func (c *Client) DeleteSeries(ctx context.Context, id int, deleteFiles, addExclusion bool) error {
|
|
q := url.Values{
|
|
"deleteFiles": {strconv.FormatBool(deleteFiles)},
|
|
"addImportListExclusion": {strconv.FormatBool(addExclusion)},
|
|
}
|
|
return c.Do(ctx, http.MethodDelete, "series/"+strconv.Itoa(id), q, nil, nil)
|
|
}
|
|
|
|
// Calendar lists the episodes airing between start and end, both inclusive,
|
|
// with their series. Unmonitored episodes are only included when asked for.
|
|
func (c *Client) Calendar(ctx context.Context, start, end time.Time, unmonitored bool) ([]Episode, error) {
|
|
q := url.Values{
|
|
"start": {start.Format("2006-01-02")},
|
|
"end": {end.Format("2006-01-02")},
|
|
"unmonitored": {strconv.FormatBool(unmonitored)},
|
|
"includeSeries": {"true"},
|
|
}
|
|
var episodes []Episode
|
|
err := c.Do(ctx, http.MethodGet, "calendar", q, nil, &episodes)
|
|
return episodes, err
|
|
}
|