Files
arr-tui/internal/sonarr/sonarr.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

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"}})
}