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>
169 lines
5.5 KiB
Go
169 lines
5.5 KiB
Go
// Package sonarr is a client for the Sonarr v3/v4 API.
|
|
package sonarr
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"net/url"
|
|
|
|
"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 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"`
|
|
}
|
|
|
|
// 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 {
|
|
body := map[string]any{"name": "SeriesSearch", "seriesId": seriesID}
|
|
return c.Do(ctx, http.MethodPost, "command", 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"}})
|
|
}
|