Files
silo-server/internal/requests/sonarr/client.go
T

184 lines
6.7 KiB
Go
Raw Normal View History

package sonarr
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
mediarequests "github.com/Silo-Server/silo-server/internal/requests"
"github.com/Silo-Server/silo-server/internal/requests/arrclient"
)
type Client struct {
httpClient *http.Client
}
type seriesResource struct {
ID int `json:"id,omitempty"`
Title string `json:"title,omitempty"`
TVDBID int `json:"tvdbId,omitempty"`
TMDBID int `json:"tmdbId,omitempty"`
TitleSlug string `json:"titleSlug,omitempty"`
QualityProfileID int `json:"qualityProfileId,omitempty"`
RootFolderPath string `json:"rootFolderPath,omitempty"`
SeasonFolder bool `json:"seasonFolder"`
Monitored bool `json:"monitored"`
SeriesType string `json:"seriesType,omitempty"`
Tags []int `json:"tags,omitempty"`
AddOptions addSeriesOptions `json:"addOptions,omitempty"`
}
type addSeriesOptions struct {
Monitor string `json:"monitor,omitempty"`
SearchForMissingEpisodes bool `json:"searchForMissingEpisodes"`
SearchForCutoffUnmetEpisodes bool `json:"searchForCutoffUnmetEpisodes,omitempty"`
}
func NewClient(httpClient *http.Client) *Client {
return &Client{httpClient: httpClient}
}
func (c *Client) ListSeriesIntegrationOptions(ctx context.Context, integration mediarequests.Integration) (*mediarequests.IntegrationOptions, error) {
client := arrclient.New(integration.BaseURL, integration.APIKeyRef, c.httpClient)
rootFolders, err := arrclient.ListRootFolders(ctx, client)
if err != nil {
return nil, err
}
qualityProfiles, err := arrclient.ListQualityProfiles(ctx, client)
if err != nil {
return nil, err
}
tags, err := arrclient.ListTags(ctx, client)
if err != nil {
return nil, err
}
return &mediarequests.IntegrationOptions{
Kind: "sonarr",
RootFolders: rootFolders,
QualityProfiles: qualityProfiles,
Tags: tags,
}, nil
}
func (c *Client) SubmitSeries(ctx context.Context, req mediarequests.Request, integration mediarequests.Integration) (mediarequests.FulfillmentResult, error) {
if req.MediaType != mediarequests.MediaTypeSeries {
return mediarequests.FulfillmentResult{}, fmt.Errorf("sonarr: request is not a series")
}
if integration.QualityProfileID == nil {
return mediarequests.FulfillmentResult{}, fmt.Errorf("sonarr: quality profile is required")
}
if req.TVDBID == nil || *req.TVDBID <= 0 {
return mediarequests.FulfillmentResult{}, fmt.Errorf("sonarr: tvdb_id is required")
}
client := arrclient.New(integration.BaseURL, integration.APIKeyRef, c.httpClient)
series, err := c.lookupSeries(ctx, client, *req.TVDBID)
if err != nil {
return mediarequests.FulfillmentResult{}, err
}
series.RootFolderPath = integration.RootFolder
series.QualityProfileID = *integration.QualityProfileID
series.SeasonFolder = arrclient.BoolOption(integration.Options, "season_folder", true)
series.Monitored = arrclient.BoolOption(integration.Options, "monitored", true)
series.SeriesType = arrclient.StringOption(integration.Options, "series_type", "standard")
series.Tags = integration.Tags
series.AddOptions = addSeriesOptions{
Monitor: arrclient.StringOption(integration.Options, "monitor", "all"),
SearchForMissingEpisodes: arrclient.BoolOption(
integration.Options,
"search_for_missing_episodes",
arrclient.BoolOption(integration.Options, "search_on_add", true),
),
SearchForCutoffUnmetEpisodes: arrclient.BoolOption(integration.Options, "search_for_cutoff_unmet", false),
}
var created seriesResource
if err := client.PostJSON(ctx, "/api/v3/series", series, &created); err != nil {
if !arrclient.IsEmptyOrTruncatedDecodeError(err) {
return mediarequests.FulfillmentResult{}, err
}
// POST accepted but Sonarr returned an empty body. Recover the
// new series' Sonarr ID by listing series filtered by TVDB ID;
// without the ID the reconcile loop cannot advance the request.
if found, lookErr := c.findSeriesByTVDBID(ctx, client, *req.TVDBID); lookErr == nil && found.ID > 0 {
return resultFromSeries(found), nil
}
return arrclient.AcceptedWithoutResponse("sonarr"), nil
}
return resultFromSeries(created), nil
}
func (c *Client) findSeriesByTVDBID(ctx context.Context, client *arrclient.Client, tvdbID int) (seriesResource, error) {
values := url.Values{}
values.Set("tvdbId", strconv.Itoa(tvdbID))
var matches []seriesResource
if err := client.GetJSON(ctx, "/api/v3/series?"+values.Encode(), &matches); err != nil {
return seriesResource{}, err
}
for _, s := range matches {
if s.ID > 0 && s.TVDBID == tvdbID {
return s, nil
}
}
return seriesResource{}, fmt.Errorf("sonarr: series not found after add for tvdb_id %d", tvdbID)
}
func (c *Client) CheckSeriesStatus(ctx context.Context, req mediarequests.Request, integration mediarequests.Integration) (mediarequests.FulfillmentStatus, error) {
client := arrclient.New(integration.BaseURL, integration.APIKeyRef, c.httpClient)
seriesID, _ := strconv.Atoi(req.ExternalID)
if seriesID <= 0 {
return mediarequests.FulfillmentStatus{
Status: mediarequests.StatusQueued,
IntegrationKind: "sonarr",
ExternalStatus: "external_id_unavailable",
}, nil
}
queues, err := c.queueDetails(ctx, client, seriesID)
if err != nil {
return mediarequests.FulfillmentStatus{}, err
}
evaluation := arrclient.EvaluateQueue(queues)
return arrclient.StatusFromQueueEvaluation("sonarr", seriesID, evaluation), nil
}
func (c *Client) lookupSeries(ctx context.Context, client *arrclient.Client, tvdbID int) (seriesResource, error) {
values := url.Values{}
values.Set("term", "tvdb:"+strconv.Itoa(tvdbID))
var matches []seriesResource
if err := client.GetJSON(ctx, "/api/v3/series/lookup?"+values.Encode(), &matches); err != nil {
return seriesResource{}, err
}
for _, match := range matches {
if match.TVDBID == tvdbID {
return match, nil
}
}
return seriesResource{}, fmt.Errorf("sonarr: no series found for tvdb_id %d", tvdbID)
}
func (c *Client) queueDetails(ctx context.Context, client *arrclient.Client, seriesID int) ([]arrclient.QueueResource, error) {
values := url.Values{}
values.Set("seriesId", strconv.Itoa(seriesID))
var queues []arrclient.QueueResource
if err := client.GetJSON(ctx, "/api/v3/queue/details?"+values.Encode(), &queues); err != nil {
return nil, err
}
return queues, nil
}
func resultFromSeries(series seriesResource) mediarequests.FulfillmentResult {
externalID := ""
if series.ID > 0 {
externalID = strconv.Itoa(series.ID)
}
return mediarequests.FulfillmentResult{
IntegrationKind: "sonarr",
ExternalID: externalID,
ExternalStatus: "queued",
}
}