feat(lists): integrate Omegabrr (#1885)

* feat(lists): integrate Omegabrr

* feat(lists): add missing lists index

* feat(lists): add db repo

* feat(lists): add db migrations

* feat(lists): labels

* feat(lists): url lists and more arrs

* fix(lists): db migrations client_id wrong type

* fix(lists): db fields

* feat(lists): create list form wip

* feat(lists): show in list and create

* feat(lists): update and delete

* feat(lists): trigger via webhook

* feat(lists): add webhook handler

* fix(arr): encode json to pointer

* feat(lists): rename endpoint to lists

* feat(lists): fetch tags from arr

* feat(lists): process plaintext lists

* feat(lists): add background refresh job

* run every 6th hour with a random start delay between 1-35 seconds

* feat(lists): refresh on save and improve logging

* feat(lists): cast arr client to pointer

* feat(lists): improve error handling

* feat(lists): reset shows field with match release

* feat(lists): filter opts all lists

* feat(lists): trigger on update if enabled

* feat(lists): update option for lists

* feat(lists): show connected filters in list

* feat(lists): missing listSvc dep

* feat(lists): cleanup

* feat(lists): typo arr list

* feat(lists): radarr include original

* feat(lists): rename ExcludeAlternateTitle to IncludeAlternateTitle

* fix(lists): arr client type conversion to pointer

* fix(actions): only log panic recover if err not nil

* feat(lists): show spinner on save

* feat(lists): show icon in filters list

* feat(lists): change icon color in filters list

* feat(lists): delete relations on filter delete
This commit is contained in:
ze0s 2024-12-25 13:23:37 +01:00 committed by GitHub
parent b68ae334ca
commit 221bc35371
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
77 changed files with 5025 additions and 254 deletions

View file

@ -15,7 +15,7 @@ import (
"github.com/autobrr/autobrr/pkg/errors"
)
func (c *client) get(ctx context.Context, endpoint string) (int, []byte, error) {
func (c *Client) get(ctx context.Context, endpoint string) (int, []byte, error) {
u, err := url.Parse(c.config.Hostname)
if err != nil {
return 0, nil, errors.Wrap(err, "could not parse url: %s", c.config.Hostname)
@ -26,7 +26,7 @@ func (c *client) get(ctx context.Context, endpoint string) (int, []byte, error)
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqUrl, http.NoBody)
if err != nil {
return 0, nil, errors.Wrap(err, "lidarr client request error : %v", reqUrl)
return 0, nil, errors.Wrap(err, "lidarr Client request error : %v", reqUrl)
}
if c.config.BasicAuth {
@ -54,7 +54,47 @@ func (c *client) get(ctx context.Context, endpoint string) (int, []byte, error)
return resp.StatusCode, buf.Bytes(), nil
}
func (c *client) post(ctx context.Context, endpoint string, data interface{}) (*http.Response, error) {
func (c *Client) getJSON(ctx context.Context, endpoint string, params url.Values, data any) error {
u, err := url.Parse(c.config.Hostname)
if err != nil {
return errors.Wrap(err, "could not parse url: %s", c.config.Hostname)
}
u.Path = path.Join(u.Path, "/api/v1/", endpoint)
reqUrl := u.String()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqUrl, http.NoBody)
if err != nil {
return errors.Wrap(err, "could not build request")
}
if c.config.BasicAuth {
req.SetBasicAuth(c.config.Username, c.config.Password)
}
c.setHeaders(req)
req.URL.RawQuery = params.Encode()
resp, err := c.http.Do(req)
if err != nil {
return errors.Wrap(err, "lidarr.http.Do(req): %+v", req)
}
defer resp.Body.Close()
if resp.Body == nil {
return errors.New("response body is nil")
}
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return errors.Wrap(err, "could not unmarshal data")
}
return nil
}
func (c *Client) post(ctx context.Context, endpoint string, data interface{}) (*http.Response, error) {
u, err := url.Parse(c.config.Hostname)
if err != nil {
return nil, errors.Wrap(err, "could not parse url: %s", c.config.Hostname)
@ -65,12 +105,12 @@ func (c *client) post(ctx context.Context, endpoint string, data interface{}) (*
jsonData, err := json.Marshal(data)
if err != nil {
return nil, errors.Wrap(err, "lidarr client could not marshal data: %v", reqUrl)
return nil, errors.Wrap(err, "lidarr Client could not marshal data: %v", reqUrl)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqUrl, bytes.NewBuffer(jsonData))
if err != nil {
return nil, errors.Wrap(err, "lidarr client request error: %v", reqUrl)
return nil, errors.Wrap(err, "lidarr Client request error: %v", reqUrl)
}
if c.config.BasicAuth {
@ -83,7 +123,7 @@ func (c *client) post(ctx context.Context, endpoint string, data interface{}) (*
res, err := c.http.Do(req)
if err != nil {
return res, errors.Wrap(err, "lidarr client request error: %v", reqUrl)
return res, errors.Wrap(err, "lidarr Client request error: %v", reqUrl)
}
// validate response
@ -97,7 +137,7 @@ func (c *client) post(ctx context.Context, endpoint string, data interface{}) (*
return res, nil
}
func (c *client) postBody(ctx context.Context, endpoint string, data interface{}) (int, []byte, error) {
func (c *Client) postBody(ctx context.Context, endpoint string, data interface{}) (int, []byte, error) {
u, err := url.Parse(c.config.Hostname)
if err != nil {
return 0, nil, errors.Wrap(err, "could not parse url: %s", c.config.Hostname)
@ -108,12 +148,12 @@ func (c *client) postBody(ctx context.Context, endpoint string, data interface{}
jsonData, err := json.Marshal(data)
if err != nil {
return 0, nil, errors.Wrap(err, "lidarr client could not marshal data: %v", reqUrl)
return 0, nil, errors.Wrap(err, "lidarr Client could not marshal data: %v", reqUrl)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, reqUrl, bytes.NewBuffer(jsonData))
if err != nil {
return 0, nil, errors.Wrap(err, "lidarr client request error: %v", reqUrl)
return 0, nil, errors.Wrap(err, "lidarr Client request error: %v", reqUrl)
}
if c.config.BasicAuth {
@ -147,7 +187,7 @@ func (c *client) postBody(ctx context.Context, endpoint string, data interface{}
return resp.StatusCode, buf.Bytes(), nil
}
func (c *client) setHeaders(req *http.Request) {
func (c *Client) setHeaders(req *http.Request) {
if req.Body != nil {
req.Header.Set("Content-Type", "application/json")
}

View file

@ -6,10 +6,11 @@ package lidarr
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"time"
@ -29,26 +30,26 @@ type Config struct {
Log *log.Logger
}
type Client interface {
type ClientInterface interface {
Test(ctx context.Context) (*SystemStatusResponse, error)
Push(ctx context.Context, release Release) ([]string, error)
}
type client struct {
type Client struct {
config Config
http *http.Client
Log *log.Logger
}
// New create new lidarr client
func New(config Config) Client {
// New create new lidarr Client
func New(config Config) *Client {
httpClient := &http.Client{
Timeout: time.Second * 120,
Transport: sharedhttp.Transport,
}
c := &client{
c := &Client{
config: config,
http: httpClient,
Log: log.New(io.Discard, "", log.LstdFlags),
@ -61,47 +62,10 @@ func New(config Config) Client {
return c
}
type Release struct {
Title string `json:"title"`
InfoUrl string `json:"infoUrl,omitempty"`
DownloadUrl string `json:"downloadUrl,omitempty"`
MagnetUrl string `json:"magnetUrl,omitempty"`
Size uint64 `json:"size"`
Indexer string `json:"indexer"`
DownloadProtocol string `json:"downloadProtocol"`
Protocol string `json:"protocol"`
PublishDate string `json:"publishDate"`
DownloadClientId int `json:"downloadClientId,omitempty"`
DownloadClient string `json:"downloadClient,omitempty"`
}
type PushResponse struct {
Approved bool `json:"approved"`
Rejected bool `json:"rejected"`
TempRejected bool `json:"temporarilyRejected"`
Rejections []string `json:"rejections"`
}
type BadRequestResponse struct {
PropertyName string `json:"propertyName"`
ErrorMessage string `json:"errorMessage"`
ErrorCode string `json:"errorCode"`
AttemptedValue string `json:"attemptedValue"`
Severity string `json:"severity"`
}
func (r BadRequestResponse) String() string {
return fmt.Sprintf("[%s: %s] %s: %s - got value: %s", r.Severity, r.ErrorCode, r.PropertyName, r.ErrorMessage, r.AttemptedValue)
}
type SystemStatusResponse struct {
Version string `json:"version"`
}
func (c *client) Test(ctx context.Context) (*SystemStatusResponse, error) {
func (c *Client) Test(ctx context.Context) (*SystemStatusResponse, error) {
status, res, err := c.get(ctx, "system/status")
if err != nil {
return nil, errors.Wrap(err, "lidarr client get error")
return nil, errors.Wrap(err, "lidarr Client get error")
}
if status == http.StatusUnauthorized {
@ -113,16 +77,16 @@ func (c *client) Test(ctx context.Context) (*SystemStatusResponse, error) {
response := SystemStatusResponse{}
err = json.Unmarshal(res, &response)
if err != nil {
return nil, errors.Wrap(err, "lidarr client error json unmarshal")
return nil, errors.Wrap(err, "lidarr Client error json unmarshal")
}
return &response, nil
}
func (c *client) Push(ctx context.Context, release Release) ([]string, error) {
func (c *Client) Push(ctx context.Context, release Release) ([]string, error) {
status, res, err := c.postBody(ctx, "release/push", release)
if err != nil {
return nil, errors.Wrap(err, "lidarr client post error")
return nil, errors.Wrap(err, "lidarr Client post error")
}
c.Log.Printf("lidarr release/push response status: %v body: %v", status, string(res))
@ -143,7 +107,7 @@ func (c *client) Push(ctx context.Context, release Release) ([]string, error) {
pushResponse := PushResponse{}
if err = json.Unmarshal(res, &pushResponse); err != nil {
return nil, errors.Wrap(err, "lidarr client error json unmarshal")
return nil, errors.Wrap(err, "lidarr Client error json unmarshal")
}
// log and return if rejected
@ -156,3 +120,28 @@ func (c *client) Push(ctx context.Context, release Release) ([]string, error) {
return nil, nil
}
func (c *Client) GetAlbums(ctx context.Context, mbID int64) ([]Album, error) {
params := make(url.Values)
if mbID != 0 {
params.Set("ForeignAlbumId", strconv.FormatInt(mbID, 10))
}
data := make([]Album, 0)
err := c.getJSON(ctx, "album", params, &data)
if err != nil {
return nil, errors.Wrap(err, "could not get tags")
}
return data, nil
}
func (c *Client) GetArtistByID(ctx context.Context, artistID int64) (*Artist, error) {
var data Artist
err := c.getJSON(ctx, "artist/"+strconv.FormatInt(artistID, 10), nil, &data)
if err != nil {
return nil, errors.Wrap(err, "could not get tags")
}
return &data, nil
}

150
pkg/arr/lidarr/types.go Normal file
View file

@ -0,0 +1,150 @@
package lidarr
import (
"fmt"
"time"
"github.com/autobrr/autobrr/pkg/arr"
)
type Release struct {
Title string `json:"title"`
InfoUrl string `json:"infoUrl,omitempty"`
DownloadUrl string `json:"downloadUrl,omitempty"`
MagnetUrl string `json:"magnetUrl,omitempty"`
Size uint64 `json:"size"`
Indexer string `json:"indexer"`
DownloadProtocol string `json:"downloadProtocol"`
Protocol string `json:"protocol"`
PublishDate string `json:"publishDate"`
DownloadClientId int `json:"downloadClientId,omitempty"`
DownloadClient string `json:"downloadClient,omitempty"`
}
type PushResponse struct {
Approved bool `json:"approved"`
Rejected bool `json:"rejected"`
TempRejected bool `json:"temporarilyRejected"`
Rejections []string `json:"rejections"`
}
type BadRequestResponse struct {
PropertyName string `json:"propertyName"`
ErrorMessage string `json:"errorMessage"`
ErrorCode string `json:"errorCode"`
AttemptedValue string `json:"attemptedValue"`
Severity string `json:"severity"`
}
func (r BadRequestResponse) String() string {
return fmt.Sprintf("[%s: %s] %s: %s - got value: %s", r.Severity, r.ErrorCode, r.PropertyName, r.ErrorMessage, r.AttemptedValue)
}
type SystemStatusResponse struct {
Version string `json:"version"`
}
type Statistics struct {
AlbumCount int `json:"albumCount,omitempty"`
TrackFileCount int `json:"trackFileCount"`
TrackCount int `json:"trackCount"`
TotalTrackCount int `json:"totalTrackCount"`
SizeOnDisk int `json:"sizeOnDisk"`
PercentOfTracks float64 `json:"percentOfTracks"`
}
type Artist struct {
ID int64 `json:"id"`
Status string `json:"status,omitempty"`
LastInfoSync time.Time `json:"lastInfoSync,omitempty"`
ArtistName string `json:"artistName,omitempty"`
ForeignArtistID string `json:"foreignArtistId,omitempty"`
TadbID int64 `json:"tadbId,omitempty"`
DiscogsID int64 `json:"discogsId,omitempty"`
QualityProfileID int64 `json:"qualityProfileId,omitempty"`
MetadataProfileID int64 `json:"metadataProfileId,omitempty"`
Overview string `json:"overview,omitempty"`
ArtistType string `json:"artistType,omitempty"`
Disambiguation string `json:"disambiguation,omitempty"`
RootFolderPath string `json:"rootFolderPath,omitempty"`
Path string `json:"path,omitempty"`
CleanName string `json:"cleanName,omitempty"`
SortName string `json:"sortName,omitempty"`
Links []*arr.Link `json:"links,omitempty"`
Images []*arr.Image `json:"images,omitempty"`
Genres []string `json:"genres,omitempty"`
Tags []int `json:"tags,omitempty"`
Added time.Time `json:"added,omitempty"`
Ratings *arr.Ratings `json:"ratings,omitempty"`
Statistics *Statistics `json:"statistics,omitempty"`
LastAlbum *Album `json:"lastAlbum,omitempty"`
NextAlbum *Album `json:"nextAlbum,omitempty"`
AddOptions *ArtistAddOptions `json:"addOptions,omitempty"`
AlbumFolder bool `json:"albumFolder,omitempty"`
Monitored bool `json:"monitored"`
Ended bool `json:"ended,omitempty"`
}
type Album struct {
ID int64 `json:"id,omitempty"`
Title string `json:"title"`
Disambiguation string `json:"disambiguation"`
Overview string `json:"overview"`
ArtistID int64 `json:"artistId"`
ForeignAlbumID string `json:"foreignAlbumId"`
ProfileID int64 `json:"profileId"`
Duration int `json:"duration"`
AlbumType string `json:"albumType"`
SecondaryTypes []interface{} `json:"secondaryTypes"`
MediumCount int `json:"mediumCount"`
Ratings *arr.Ratings `json:"ratings"`
ReleaseDate time.Time `json:"releaseDate"`
Releases []*AlbumRelease `json:"releases"`
Genres []interface{} `json:"genres"`
Media []*Media `json:"media"`
Artist *Artist `json:"artist"`
Links []*arr.Link `json:"links"`
Images []*arr.Image `json:"images"`
Statistics *Statistics `json:"statistics"`
RemoteCover string `json:"remoteCover,omitempty"`
AddOptions *AlbumAddOptions `json:"addOptions,omitempty"`
Monitored bool `json:"monitored"`
AnyReleaseOk bool `json:"anyReleaseOk"`
Grabbed bool `json:"grabbed"`
}
// Release is part of an Album.
type AlbumRelease struct {
ID int64 `json:"id"`
AlbumID int64 `json:"albumId"`
ForeignReleaseID string `json:"foreignReleaseId"`
Title string `json:"title"`
Status string `json:"status"`
Duration int `json:"duration"`
TrackCount int `json:"trackCount"`
Media []*Media `json:"media"`
MediumCount int `json:"mediumCount"`
Disambiguation string `json:"disambiguation"`
Country []string `json:"country"`
Label []string `json:"label"`
Format string `json:"format"`
Monitored bool `json:"monitored"`
}
// Media is part of an Album.
type Media struct {
MediumNumber int64 `json:"mediumNumber"`
MediumName string `json:"mediumName"`
MediumFormat string `json:"mediumFormat"`
}
// ArtistAddOptions is part of an artist and an album.
type ArtistAddOptions struct {
Monitor string `json:"monitor,omitempty"`
Monitored bool `json:"monitored,omitempty"`
SearchForMissingAlbums bool `json:"searchForMissingAlbums,omitempty"`
}
type AlbumAddOptions struct {
SearchForNewAlbum bool `json:"searchForNewAlbum,omitempty"`
}

View file

@ -15,7 +15,7 @@ import (
"github.com/autobrr/autobrr/pkg/errors"
)
func (c *client) get(ctx context.Context, endpoint string) (int, []byte, error) {
func (c *Client) get(ctx context.Context, endpoint string) (int, []byte, error) {
u, err := url.Parse(c.config.Hostname)
if err != nil {
return 0, nil, errors.Wrap(err, "could not parse url: %s", c.config.Hostname)
@ -54,7 +54,47 @@ func (c *client) get(ctx context.Context, endpoint string) (int, []byte, error)
return resp.StatusCode, buf.Bytes(), nil
}
func (c *client) post(ctx context.Context, endpoint string, data interface{}) (*http.Response, error) {
func (c *Client) getJSON(ctx context.Context, endpoint string, params url.Values, data any) error {
u, err := url.Parse(c.config.Hostname)
if err != nil {
return errors.Wrap(err, "could not parse url: %s", c.config.Hostname)
}
u.Path = path.Join(u.Path, "/api/v3/", endpoint)
reqUrl := u.String()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqUrl, http.NoBody)
if err != nil {
return errors.Wrap(err, "could not build request")
}
if c.config.BasicAuth {
req.SetBasicAuth(c.config.Username, c.config.Password)
}
c.setHeaders(req)
req.URL.RawQuery = params.Encode()
resp, err := c.http.Do(req)
if err != nil {
return errors.Wrap(err, "radarr.http.Do(req): %+v", req)
}
defer resp.Body.Close()
if resp.Body == nil {
return errors.New("response body is nil")
}
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return errors.Wrap(err, "could not unmarshal data")
}
return nil
}
func (c *Client) post(ctx context.Context, endpoint string, data interface{}) (*http.Response, error) {
u, err := url.Parse(c.config.Hostname)
if err != nil {
return nil, errors.Wrap(err, "could not parse url: %s", c.config.Hostname)
@ -99,7 +139,7 @@ func (c *client) post(ctx context.Context, endpoint string, data interface{}) (*
return res, nil
}
func (c *client) postBody(ctx context.Context, endpoint string, data interface{}) (int, []byte, error) {
func (c *Client) postBody(ctx context.Context, endpoint string, data interface{}) (int, []byte, error) {
u, err := url.Parse(c.config.Hostname)
if err != nil {
return 0, nil, errors.Wrap(err, "could not parse url: %s", c.config.Hostname)
@ -149,7 +189,7 @@ func (c *client) postBody(ctx context.Context, endpoint string, data interface{}
return resp.StatusCode, buf.Bytes(), nil
}
func (c *client) setHeaders(req *http.Request) {
func (c *Client) setHeaders(req *http.Request) {
if req.Body != nil {
req.Header.Set("Content-Type", "application/json")
}

View file

@ -6,13 +6,15 @@ package radarr
import (
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"github.com/autobrr/autobrr/pkg/arr"
"github.com/autobrr/autobrr/pkg/errors"
"github.com/autobrr/autobrr/pkg/sharedhttp"
)
@ -29,25 +31,25 @@ type Config struct {
Log *log.Logger
}
type Client interface {
type ClientInterface interface {
Test(ctx context.Context) (*SystemStatusResponse, error)
Push(ctx context.Context, release Release) ([]string, error)
}
type client struct {
type Client struct {
config Config
http *http.Client
Log *log.Logger
}
func New(config Config) Client {
func New(config Config) *Client {
httpClient := &http.Client{
Timeout: time.Second * 120,
Transport: sharedhttp.Transport,
}
c := &client{
c := &Client{
config: config,
http: httpClient,
Log: log.New(io.Discard, "", log.LstdFlags),
@ -60,44 +62,7 @@ func New(config Config) Client {
return c
}
type Release struct {
Title string `json:"title"`
InfoUrl string `json:"infoUrl,omitempty"`
DownloadUrl string `json:"downloadUrl,omitempty"`
MagnetUrl string `json:"magnetUrl,omitempty"`
Size uint64 `json:"size"`
Indexer string `json:"indexer"`
DownloadProtocol string `json:"downloadProtocol"`
Protocol string `json:"protocol"`
PublishDate string `json:"publishDate"`
DownloadClientId int `json:"downloadClientId,omitempty"`
DownloadClient string `json:"downloadClient,omitempty"`
}
type PushResponse struct {
Approved bool `json:"approved"`
Rejected bool `json:"rejected"`
TempRejected bool `json:"temporarilyRejected"`
Rejections []string `json:"rejections"`
}
type SystemStatusResponse struct {
Version string `json:"version"`
}
type BadRequestResponse struct {
Severity string `json:"severity"`
ErrorCode string `json:"errorCode"`
ErrorMessage string `json:"errorMessage"`
PropertyName string `json:"propertyName"`
AttemptedValue string `json:"attemptedValue"`
}
func (r *BadRequestResponse) String() string {
return fmt.Sprintf("[%s: %s] %s: %s - got value: %s", r.Severity, r.ErrorCode, r.PropertyName, r.ErrorMessage, r.AttemptedValue)
}
func (c *client) Test(ctx context.Context) (*SystemStatusResponse, error) {
func (c *Client) Test(ctx context.Context) (*SystemStatusResponse, error) {
status, res, err := c.get(ctx, "system/status")
if err != nil {
return nil, errors.Wrap(err, "radarr error running test")
@ -117,7 +82,7 @@ func (c *client) Test(ctx context.Context) (*SystemStatusResponse, error) {
return &response, nil
}
func (c *client) Push(ctx context.Context, release Release) ([]string, error) {
func (c *Client) Push(ctx context.Context, release Release) ([]string, error) {
status, res, err := c.postBody(ctx, "release/push", release)
if err != nil {
return nil, errors.Wrap(err, "error push release")
@ -155,3 +120,28 @@ func (c *client) Push(ctx context.Context, release Release) ([]string, error) {
// success true
return nil, nil
}
func (c *Client) GetMovies(ctx context.Context, tmdbID int64) ([]Movie, error) {
params := make(url.Values)
if tmdbID != 0 {
params.Set("tmdbId", strconv.FormatInt(tmdbID, 10))
}
data := make([]Movie, 0)
err := c.getJSON(ctx, "movie", params, &data)
if err != nil {
return nil, errors.Wrap(err, "could not get tags")
}
return data, nil
}
func (c *Client) GetTags(ctx context.Context) ([]*arr.Tag, error) {
data := make([]*arr.Tag, 0)
err := c.getJSON(ctx, "tag", nil, &data)
if err != nil {
return nil, errors.Wrap(err, "could not get tags")
}
return data, nil
}

135
pkg/arr/radarr/types.go Normal file
View file

@ -0,0 +1,135 @@
package radarr
import (
"fmt"
"time"
"github.com/autobrr/autobrr/pkg/arr"
)
type Movie struct {
ID int64 `json:"id"`
Title string `json:"title,omitempty"`
Path string `json:"path,omitempty"`
MinimumAvailability string `json:"minimumAvailability,omitempty"`
QualityProfileID int64 `json:"qualityProfileId,omitempty"`
TmdbID int64 `json:"tmdbId,omitempty"`
OriginalTitle string `json:"originalTitle,omitempty"`
AlternateTitles []*AlternativeTitle `json:"alternateTitles,omitempty"`
SecondaryYearSourceID int `json:"secondaryYearSourceId,omitempty"`
SortTitle string `json:"sortTitle,omitempty"`
SizeOnDisk int64 `json:"sizeOnDisk,omitempty"`
Status string `json:"status,omitempty"`
Overview string `json:"overview,omitempty"`
InCinemas time.Time `json:"inCinemas,omitempty"`
PhysicalRelease time.Time `json:"physicalRelease,omitempty"`
DigitalRelease time.Time `json:"digitalRelease,omitempty"`
Images []*arr.Image `json:"images,omitempty"`
Website string `json:"website,omitempty"`
Year int `json:"year,omitempty"`
YouTubeTrailerID string `json:"youTubeTrailerId,omitempty"`
Studio string `json:"studio,omitempty"`
FolderName string `json:"folderName,omitempty"`
Runtime int `json:"runtime,omitempty"`
CleanTitle string `json:"cleanTitle,omitempty"`
ImdbID string `json:"imdbId,omitempty"`
TitleSlug string `json:"titleSlug,omitempty"`
Certification string `json:"certification,omitempty"`
Genres []string `json:"genres,omitempty"`
Tags []int `json:"tags,omitempty"`
Added time.Time `json:"added,omitempty"`
Ratings *arr.Ratings `json:"ratings,omitempty"`
MovieFile *MovieFile `json:"movieFile,omitempty"`
Collection *Collection `json:"collection,omitempty"`
HasFile bool `json:"hasFile,omitempty"`
IsAvailable bool `json:"isAvailable,omitempty"`
Monitored bool `json:"monitored"`
}
type AlternativeTitle struct {
MovieID int `json:"movieId"`
Title string `json:"title"`
SourceType string `json:"sourceType"`
SourceID int `json:"sourceId"`
Votes int `json:"votes"`
VoteCount int `json:"voteCount"`
Language *arr.Value `json:"language"`
ID int `json:"id"`
}
type MovieFile struct {
ID int64 `json:"id"`
MovieID int64 `json:"movieId"`
RelativePath string `json:"relativePath"`
Path string `json:"path"`
Size int64 `json:"size"`
DateAdded time.Time `json:"dateAdded"`
SceneName string `json:"sceneName"`
IndexerFlags int64 `json:"indexerFlags"`
Quality *arr.Quality `json:"quality"`
MediaInfo *MediaInfo `json:"mediaInfo"`
QualityCutoffNotMet bool `json:"qualityCutoffNotMet"`
Languages []*arr.Value `json:"languages"`
ReleaseGroup string `json:"releaseGroup"`
Edition string `json:"edition"`
}
type MediaInfo struct {
AudioAdditionalFeatures string `json:"audioAdditionalFeatures"`
AudioBitrate int `json:"audioBitrate"`
AudioChannels float64 `json:"audioChannels"`
AudioCodec string `json:"audioCodec"`
AudioLanguages string `json:"audioLanguages"`
AudioStreamCount int `json:"audioStreamCount"`
VideoBitDepth int `json:"videoBitDepth"`
VideoBitrate int `json:"videoBitrate"`
VideoCodec string `json:"videoCodec"`
VideoFps float64 `json:"videoFps"`
Resolution string `json:"resolution"`
RunTime string `json:"runTime"`
ScanType string `json:"scanType"`
Subtitles string `json:"subtitles"`
}
type Collection struct {
Name string `json:"name"`
TmdbID int64 `json:"tmdbId"`
Images []*arr.Image `json:"images"`
}
type Release struct {
Title string `json:"title"`
InfoUrl string `json:"infoUrl,omitempty"`
DownloadUrl string `json:"downloadUrl,omitempty"`
MagnetUrl string `json:"magnetUrl,omitempty"`
Size uint64 `json:"size"`
Indexer string `json:"indexer"`
DownloadProtocol string `json:"downloadProtocol"`
Protocol string `json:"protocol"`
PublishDate string `json:"publishDate"`
DownloadClientId int `json:"downloadClientId,omitempty"`
DownloadClient string `json:"downloadClient,omitempty"`
}
type PushResponse struct {
Approved bool `json:"approved"`
Rejected bool `json:"rejected"`
TempRejected bool `json:"temporarilyRejected"`
Rejections []string `json:"rejections"`
}
type SystemStatusResponse struct {
Version string `json:"version"`
}
type BadRequestResponse struct {
Severity string `json:"severity"`
ErrorCode string `json:"errorCode"`
ErrorMessage string `json:"errorMessage"`
PropertyName string `json:"propertyName"`
AttemptedValue string `json:"attemptedValue"`
}
func (r *BadRequestResponse) String() string {
return fmt.Sprintf("[%s: %s] %s: %s - got value: %s", r.Severity, r.ErrorCode, r.PropertyName, r.ErrorMessage, r.AttemptedValue)
}

View file

@ -15,7 +15,7 @@ import (
"github.com/autobrr/autobrr/pkg/errors"
)
func (c *client) get(ctx context.Context, endpoint string) (int, []byte, error) {
func (c *Client) get(ctx context.Context, endpoint string) (int, []byte, error) {
u, err := url.Parse(c.config.Hostname)
if err != nil {
return 0, nil, errors.Wrap(err, "could not parse url: %s", c.config.Hostname)
@ -54,7 +54,47 @@ func (c *client) get(ctx context.Context, endpoint string) (int, []byte, error)
return resp.StatusCode, buf.Bytes(), nil
}
func (c *client) post(ctx context.Context, endpoint string, data interface{}) (*http.Response, error) {
func (c *Client) getJSON(ctx context.Context, endpoint string, params url.Values, data any) error {
u, err := url.Parse(c.config.Hostname)
if err != nil {
return errors.Wrap(err, "could not parse url: %s", c.config.Hostname)
}
u.Path = path.Join(u.Path, "/api/v1/", endpoint)
reqUrl := u.String()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqUrl, http.NoBody)
if err != nil {
return errors.Wrap(err, "could not build request")
}
if c.config.BasicAuth {
req.SetBasicAuth(c.config.Username, c.config.Password)
}
c.setHeaders(req)
req.URL.RawQuery = params.Encode()
resp, err := c.http.Do(req)
if err != nil {
return errors.Wrap(err, "readarr.http.Do(req): %+v", req)
}
defer resp.Body.Close()
if resp.Body == nil {
return errors.New("response body is nil")
}
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return errors.Wrap(err, "could not unmarshal data")
}
return nil
}
func (c *Client) post(ctx context.Context, endpoint string, data interface{}) (*http.Response, error) {
u, err := url.Parse(c.config.Hostname)
if err != nil {
return nil, errors.Wrap(err, "could not parse url: %s", c.config.Hostname)
@ -97,7 +137,7 @@ func (c *client) post(ctx context.Context, endpoint string, data interface{}) (*
return res, nil
}
func (c *client) postBody(ctx context.Context, endpoint string, data interface{}) (int, []byte, error) {
func (c *Client) postBody(ctx context.Context, endpoint string, data interface{}) (int, []byte, error) {
u, err := url.Parse(c.config.Hostname)
if err != nil {
return 0, nil, errors.Wrap(err, "could not parse url: %s", c.config.Hostname)
@ -149,7 +189,7 @@ func (c *client) postBody(ctx context.Context, endpoint string, data interface{}
return resp.StatusCode, buf.Bytes(), nil
}
func (c *client) setHeaders(req *http.Request) {
func (c *Client) setHeaders(req *http.Request) {
if req.Body != nil {
req.Header.Set("Content-Type", "application/json")
}

View file

@ -6,14 +6,15 @@ package readarr
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
"log"
"github.com/autobrr/autobrr/pkg/arr"
"github.com/autobrr/autobrr/pkg/errors"
"github.com/autobrr/autobrr/pkg/sharedhttp"
)
@ -30,26 +31,26 @@ type Config struct {
Log *log.Logger
}
type Client interface {
type ClientInterface interface {
Test(ctx context.Context) (*SystemStatusResponse, error)
Push(ctx context.Context, release Release) ([]string, error)
}
type client struct {
type Client struct {
config Config
http *http.Client
Log *log.Logger
}
// New create new readarr client
func New(config Config) Client {
// New create new readarr Client
func New(config Config) *Client {
httpClient := &http.Client{
Timeout: time.Second * 120,
Transport: sharedhttp.Transport,
}
c := &client{
c := &Client{
config: config,
http: httpClient,
Log: log.New(io.Discard, "", log.LstdFlags),
@ -62,45 +63,7 @@ func New(config Config) Client {
return c
}
type Release struct {
Title string `json:"title"`
InfoUrl string `json:"infoUrl,omitempty"`
DownloadUrl string `json:"downloadUrl,omitempty"`
MagnetUrl string `json:"magnetUrl,omitempty"`
Size uint64 `json:"size"`
Indexer string `json:"indexer"`
DownloadProtocol string `json:"downloadProtocol"`
Protocol string `json:"protocol"`
PublishDate string `json:"publishDate"`
DownloadClientId int `json:"downloadClientId,omitempty"`
DownloadClient string `json:"downloadClient,omitempty"`
}
type PushResponse struct {
Approved bool `json:"approved"`
Rejected bool `json:"rejected"`
TempRejected bool `json:"temporarilyRejected"`
Rejections []string `json:"rejections"`
}
type BadRequestResponse struct {
PropertyName string `json:"propertyName"`
ErrorMessage string `json:"errorMessage"`
ErrorCode string `json:"errorCode"`
AttemptedValue string `json:"attemptedValue"`
Severity string `json:"severity"`
}
func (r *BadRequestResponse) String() string {
return fmt.Sprintf("[%s: %s] %s: %s - got value: %s", r.Severity, r.ErrorCode, r.PropertyName, r.ErrorMessage, r.AttemptedValue)
}
type SystemStatusResponse struct {
AppName string `json:"appName"`
Version string `json:"version"`
}
func (c *client) Test(ctx context.Context) (*SystemStatusResponse, error) {
func (c *Client) Test(ctx context.Context) (*SystemStatusResponse, error) {
status, res, err := c.get(ctx, "system/status")
if err != nil {
return nil, errors.Wrap(err, "could not make Test")
@ -120,7 +83,7 @@ func (c *client) Test(ctx context.Context) (*SystemStatusResponse, error) {
return &response, nil
}
func (c *client) Push(ctx context.Context, release Release) ([]string, error) {
func (c *Client) Push(ctx context.Context, release Release) ([]string, error) {
status, res, err := c.postBody(ctx, "release/push", release)
if err != nil {
return nil, errors.Wrap(err, "could not push release to readarr")
@ -160,3 +123,28 @@ func (c *client) Push(ctx context.Context, release Release) ([]string, error) {
// successful push
return nil, nil
}
func (c *Client) GetBooks(ctx context.Context, gridID string) ([]Book, error) {
params := make(url.Values)
if gridID != "" {
params.Set("titleSlug", gridID)
}
data := make([]Book, 0)
err := c.getJSON(ctx, "book", params, &data)
if err != nil {
return nil, errors.Wrap(err, "could not get tags")
}
return data, nil
}
func (c *Client) GetTags(ctx context.Context) ([]*arr.Tag, error) {
data := make([]*arr.Tag, 0)
err := c.getJSON(ctx, "tag", nil, &data)
if err != nil {
return nil, errors.Wrap(err, "could not get tags")
}
return data, nil
}

122
pkg/arr/readarr/types.go Normal file
View file

@ -0,0 +1,122 @@
package readarr
import (
"fmt"
"github.com/autobrr/autobrr/pkg/arr"
"time"
)
type Release struct {
Title string `json:"title"`
InfoUrl string `json:"infoUrl,omitempty"`
DownloadUrl string `json:"downloadUrl,omitempty"`
MagnetUrl string `json:"magnetUrl,omitempty"`
Size uint64 `json:"size"`
Indexer string `json:"indexer"`
DownloadProtocol string `json:"downloadProtocol"`
Protocol string `json:"protocol"`
PublishDate string `json:"publishDate"`
DownloadClientId int `json:"downloadClientId,omitempty"`
DownloadClient string `json:"downloadClient,omitempty"`
}
type PushResponse struct {
Approved bool `json:"approved"`
Rejected bool `json:"rejected"`
TempRejected bool `json:"temporarilyRejected"`
Rejections []string `json:"rejections"`
}
type BadRequestResponse struct {
PropertyName string `json:"propertyName"`
ErrorMessage string `json:"errorMessage"`
ErrorCode string `json:"errorCode"`
AttemptedValue string `json:"attemptedValue"`
Severity string `json:"severity"`
}
func (r *BadRequestResponse) String() string {
return fmt.Sprintf("[%s: %s] %s: %s - got value: %s", r.Severity, r.ErrorCode, r.PropertyName, r.ErrorMessage, r.AttemptedValue)
}
type SystemStatusResponse struct {
AppName string `json:"appName"`
Version string `json:"version"`
}
type Book struct {
Title string `json:"title"`
SeriesTitle string `json:"seriesTitle"`
Overview string `json:"overview"`
AuthorID int64 `json:"authorId"`
ForeignBookID string `json:"foreignBookId"`
TitleSlug string `json:"titleSlug"`
Monitored bool `json:"monitored"`
AnyEditionOk bool `json:"anyEditionOk"`
Ratings *arr.Ratings `json:"ratings"`
ReleaseDate time.Time `json:"releaseDate"`
PageCount int `json:"pageCount"`
Genres []interface{} `json:"genres"`
Author *BookAuthor `json:"author,omitempty"`
Images []*arr.Image `json:"images"`
Links []*arr.Link `json:"links"`
Statistics *Statistics `json:"statistics,omitempty"`
Editions []*Edition `json:"editions"`
ID int64 `json:"id"`
Disambiguation string `json:"disambiguation,omitempty"`
}
// Statistics for a Book, or maybe an author.
type Statistics struct {
BookCount int `json:"bookCount"`
BookFileCount int `json:"bookFileCount"`
TotalBookCount int `json:"totalBookCount"`
SizeOnDisk int `json:"sizeOnDisk"`
PercentOfBooks float64 `json:"percentOfBooks"`
}
// BookAuthor of a Book.
type BookAuthor struct {
ID int64 `json:"id"`
Status string `json:"status"`
AuthorName string `json:"authorName"`
ForeignAuthorID string `json:"foreignAuthorId"`
TitleSlug string `json:"titleSlug"`
Overview string `json:"overview"`
Links []*arr.Link `json:"links"`
Images []*arr.Image `json:"images"`
Path string `json:"path"`
QualityProfileID int64 `json:"qualityProfileId"`
MetadataProfileID int64 `json:"metadataProfileId"`
Genres []interface{} `json:"genres"`
CleanName string `json:"cleanName"`
SortName string `json:"sortName"`
Tags []int `json:"tags"`
Added time.Time `json:"added"`
Ratings *arr.Ratings `json:"ratings"`
Statistics *Statistics `json:"statistics"`
Monitored bool `json:"monitored"`
Ended bool `json:"ended"`
}
// Edition is more Book meta data.
type Edition struct {
ID int64 `json:"id"`
BookID int64 `json:"bookId"`
ForeignEditionID string `json:"foreignEditionId"`
TitleSlug string `json:"titleSlug"`
Isbn13 string `json:"isbn13"`
Asin string `json:"asin"`
Title string `json:"title"`
Overview string `json:"overview"`
Format string `json:"format"`
Publisher string `json:"publisher"`
PageCount int `json:"pageCount"`
ReleaseDate time.Time `json:"releaseDate"`
Images []*arr.Image `json:"images"`
Links []*arr.Link `json:"links"`
Ratings *arr.Ratings `json:"ratings"`
Monitored bool `json:"monitored"`
ManualAdd bool `json:"manualAdd"`
IsEbook bool `json:"isEbook"`
}

57
pkg/arr/shared.go Normal file
View file

@ -0,0 +1,57 @@
package arr
type Tag struct {
ID int
Label string
}
type Link struct {
URL string `json:"url"`
Name string `json:"name"`
}
type Image struct {
CoverType string `json:"coverType"`
URL string `json:"url"`
RemoteURL string `json:"remoteUrl,omitempty"`
Extension string `json:"extension,omitempty"`
}
type Ratings struct {
Votes int64 `json:"votes"`
Value float64 `json:"value"`
Popularity float64 `json:"popularity,omitempty"`
}
type Value struct {
ID int64 `json:"id"`
Name string `json:"name"`
}
// BaseQuality is a base quality profile.
type BaseQuality struct {
ID int64 `json:"id"`
Name string `json:"name"`
Source string `json:"source,omitempty"`
Resolution int `json:"resolution,omitempty"`
Modifier string `json:"modifier,omitempty"`
}
// Quality is a download quality profile attached to a movie, book, track or series.
// It may contain 1 or more profiles.
// Sonarr nor Readarr use Name or ID in this struct.
type Quality struct {
Name string `json:"name,omitempty"`
ID int `json:"id,omitempty"`
Quality *BaseQuality `json:"quality,omitempty"`
Items []*Quality `json:"items,omitempty"`
Allowed bool `json:"allowed"`
Revision *QualityRevision `json:"revision,omitempty"` // Not sure which app had this....
}
// QualityRevision is probably used in Sonarr.
type QualityRevision struct {
Version int64 `json:"version"`
Real int64 `json:"real"`
IsRepack bool `json:"isRepack,omitempty"`
}

View file

@ -15,7 +15,7 @@ import (
"github.com/autobrr/autobrr/pkg/errors"
)
func (c *client) get(ctx context.Context, endpoint string) (int, []byte, error) {
func (c *Client) get(ctx context.Context, endpoint string) (int, []byte, error) {
u, err := url.Parse(c.config.Hostname)
if err != nil {
return 0, nil, errors.Wrap(err, "could not parse url: %s", c.config.Hostname)
@ -54,7 +54,47 @@ func (c *client) get(ctx context.Context, endpoint string) (int, []byte, error)
return resp.StatusCode, buf.Bytes(), nil
}
func (c *client) post(ctx context.Context, endpoint string, data interface{}) (*http.Response, error) {
func (c *Client) getJSON(ctx context.Context, endpoint string, params url.Values, data any) error {
u, err := url.Parse(c.config.Hostname)
if err != nil {
return errors.Wrap(err, "could not parse url: %s", c.config.Hostname)
}
u.Path = path.Join(u.Path, "/api/v3/", endpoint)
reqUrl := u.String()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, reqUrl, http.NoBody)
if err != nil {
return errors.Wrap(err, "could not build request")
}
if c.config.BasicAuth {
req.SetBasicAuth(c.config.Username, c.config.Password)
}
c.setHeaders(req)
req.URL.RawQuery = params.Encode()
resp, err := c.http.Do(req)
if err != nil {
return errors.Wrap(err, "sonarr.http.Do(req): %+v", req)
}
defer resp.Body.Close()
if resp.Body == nil {
return errors.New("response body is nil")
}
if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
return errors.Wrap(err, "could not unmarshal data")
}
return nil
}
func (c *Client) post(ctx context.Context, endpoint string, data interface{}) (*http.Response, error) {
u, err := url.Parse(c.config.Hostname)
if err != nil {
return nil, errors.Wrap(err, "could not parse url: %s", c.config.Hostname)
@ -97,7 +137,7 @@ func (c *client) post(ctx context.Context, endpoint string, data interface{}) (*
return res, nil
}
func (c *client) postBody(ctx context.Context, endpoint string, data interface{}) (int, []byte, error) {
func (c *Client) postBody(ctx context.Context, endpoint string, data interface{}) (int, []byte, error) {
u, err := url.Parse(c.config.Hostname)
if err != nil {
return 0, nil, errors.Wrap(err, "could not parse url: %s", c.config.Hostname)
@ -147,7 +187,7 @@ func (c *client) postBody(ctx context.Context, endpoint string, data interface{}
return resp.StatusCode, buf.Bytes(), nil
}
func (c *client) setHeaders(req *http.Request) {
func (c *Client) setHeaders(req *http.Request) {
if req.Body != nil {
req.Header.Set("Content-Type", "application/json")
}

View file

@ -6,14 +6,16 @@ package sonarr
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"log"
"github.com/autobrr/autobrr/pkg/arr"
"github.com/autobrr/autobrr/pkg/errors"
"github.com/autobrr/autobrr/pkg/sharedhttp"
)
@ -30,26 +32,26 @@ type Config struct {
Log *log.Logger
}
type Client interface {
type ClientInterface interface {
Test(ctx context.Context) (*SystemStatusResponse, error)
Push(ctx context.Context, release Release) ([]string, error)
}
type client struct {
type Client struct {
config Config
http *http.Client
Log *log.Logger
}
// New create new sonarr client
func New(config Config) Client {
// New create new sonarr Client
func New(config Config) *Client {
httpClient := &http.Client{
Timeout: time.Second * 120,
Transport: sharedhttp.Transport,
}
c := &client{
c := &Client{
config: config,
http: httpClient,
Log: log.New(io.Discard, "", log.LstdFlags),
@ -62,44 +64,7 @@ func New(config Config) Client {
return c
}
type Release struct {
Title string `json:"title"`
InfoUrl string `json:"infoUrl,omitempty"`
DownloadUrl string `json:"downloadUrl,omitempty"`
MagnetUrl string `json:"magnetUrl,omitempty"`
Size uint64 `json:"size"`
Indexer string `json:"indexer"`
DownloadProtocol string `json:"downloadProtocol"`
Protocol string `json:"protocol"`
PublishDate string `json:"publishDate"`
DownloadClientId int `json:"downloadClientId,omitempty"`
DownloadClient string `json:"downloadClient,omitempty"`
}
type PushResponse struct {
Approved bool `json:"approved"`
Rejected bool `json:"rejected"`
TempRejected bool `json:"temporarilyRejected"`
Rejections []string `json:"rejections"`
}
type BadRequestResponse struct {
PropertyName string `json:"propertyName"`
ErrorMessage string `json:"errorMessage"`
ErrorCode string `json:"errorCode"`
AttemptedValue string `json:"attemptedValue"`
Severity string `json:"severity"`
}
func (r *BadRequestResponse) String() string {
return fmt.Sprintf("[%s: %s] %s: %s - got value: %s", r.Severity, r.ErrorCode, r.PropertyName, r.ErrorMessage, r.AttemptedValue)
}
type SystemStatusResponse struct {
Version string `json:"version"`
}
func (c *client) Test(ctx context.Context) (*SystemStatusResponse, error) {
func (c *Client) Test(ctx context.Context) (*SystemStatusResponse, error) {
status, res, err := c.get(ctx, "system/status")
if err != nil {
return nil, errors.Wrap(err, "could not make Test")
@ -119,7 +84,7 @@ func (c *client) Test(ctx context.Context) (*SystemStatusResponse, error) {
return &response, nil
}
func (c *client) Push(ctx context.Context, release Release) ([]string, error) {
func (c *Client) Push(ctx context.Context, release Release) ([]string, error) {
status, res, err := c.postBody(ctx, "release/push", release)
if err != nil {
return nil, errors.Wrap(err, "could not push release to sonarr")
@ -158,3 +123,32 @@ func (c *client) Push(ctx context.Context, release Release) ([]string, error) {
// successful push
return nil, nil
}
func (c *Client) GetAllSeries(ctx context.Context) ([]Series, error) {
return c.GetSeries(ctx, 0)
}
func (c *Client) GetSeries(ctx context.Context, tvdbID int64) ([]Series, error) {
params := make(url.Values)
if tvdbID != 0 {
params.Set("tvdbId", strconv.FormatInt(tvdbID, 10))
}
data := make([]Series, 0)
err := c.getJSON(ctx, "series", params, &data)
if err != nil {
return nil, errors.Wrap(err, "could not get tags")
}
return data, nil
}
func (c *Client) GetTags(ctx context.Context) ([]*arr.Tag, error) {
data := make([]*arr.Tag, 0)
err := c.getJSON(ctx, "tag", nil, &data)
if err != nil {
return nil, errors.Wrap(err, "could not get tags")
}
return data, nil
}

105
pkg/arr/sonarr/types.go Normal file
View file

@ -0,0 +1,105 @@
package sonarr
import (
"fmt"
"time"
"github.com/autobrr/autobrr/pkg/arr"
)
type Release struct {
Title string `json:"title"`
InfoUrl string `json:"infoUrl,omitempty"`
DownloadUrl string `json:"downloadUrl,omitempty"`
MagnetUrl string `json:"magnetUrl,omitempty"`
Size uint64 `json:"size"`
Indexer string `json:"indexer"`
DownloadProtocol string `json:"downloadProtocol"`
Protocol string `json:"protocol"`
PublishDate string `json:"publishDate"`
DownloadClientId int `json:"downloadClientId,omitempty"`
DownloadClient string `json:"downloadClient,omitempty"`
}
type PushResponse struct {
Approved bool `json:"approved"`
Rejected bool `json:"rejected"`
TempRejected bool `json:"temporarilyRejected"`
Rejections []string `json:"rejections"`
}
type BadRequestResponse struct {
PropertyName string `json:"propertyName"`
ErrorMessage string `json:"errorMessage"`
ErrorCode string `json:"errorCode"`
AttemptedValue string `json:"attemptedValue"`
Severity string `json:"severity"`
}
func (r *BadRequestResponse) String() string {
return fmt.Sprintf("[%s: %s] %s: %s - got value: %s", r.Severity, r.ErrorCode, r.PropertyName, r.ErrorMessage, r.AttemptedValue)
}
type SystemStatusResponse struct {
Version string `json:"version"`
}
type AlternateTitle struct {
Title string `json:"title"`
SeasonNumber int `json:"seasonNumber"`
}
type Season struct {
SeasonNumber int `json:"seasonNumber"`
Monitored bool `json:"monitored"`
Statistics *Statistics `json:"statistics,omitempty"`
}
type Statistics struct {
SeasonCount int `json:"seasonCount"`
PreviousAiring time.Time `json:"previousAiring"`
EpisodeFileCount int `json:"episodeFileCount"`
EpisodeCount int `json:"episodeCount"`
TotalEpisodeCount int `json:"totalEpisodeCount"`
SizeOnDisk int64 `json:"sizeOnDisk"`
PercentOfEpisodes float64 `json:"percentOfEpisodes"`
}
type Series struct {
ID int64 `json:"id"`
Title string `json:"title,omitempty"`
AlternateTitles []*AlternateTitle `json:"alternateTitles,omitempty"`
SortTitle string `json:"sortTitle,omitempty"`
Status string `json:"status,omitempty"`
Overview string `json:"overview,omitempty"`
PreviousAiring time.Time `json:"previousAiring,omitempty"`
Network string `json:"network,omitempty"`
Images []*arr.Image `json:"images,omitempty"`
Seasons []*Season `json:"seasons,omitempty"`
Year int `json:"year,omitempty"`
Path string `json:"path,omitempty"`
QualityProfileID int64 `json:"qualityProfileId,omitempty"`
LanguageProfileID int64 `json:"languageProfileId,omitempty"`
Runtime int `json:"runtime,omitempty"`
TvdbID int64 `json:"tvdbId,omitempty"`
TvRageID int64 `json:"tvRageId,omitempty"`
TvMazeID int64 `json:"tvMazeId,omitempty"`
FirstAired time.Time `json:"firstAired,omitempty"`
SeriesType string `json:"seriesType,omitempty"`
CleanTitle string `json:"cleanTitle,omitempty"`
ImdbID string `json:"imdbId,omitempty"`
TitleSlug string `json:"titleSlug,omitempty"`
RootFolderPath string `json:"rootFolderPath,omitempty"`
Certification string `json:"certification,omitempty"`
Genres []string `json:"genres,omitempty"`
Tags []int `json:"tags,omitempty"`
Added time.Time `json:"added,omitempty"`
Ratings *arr.Ratings `json:"ratings,omitempty"`
Statistics *Statistics `json:"statistics,omitempty"`
NextAiring time.Time `json:"nextAiring,omitempty"`
AirTime string `json:"airTime,omitempty"`
Ended bool `json:"ended,omitempty"`
SeasonFolder bool `json:"seasonFolder,omitempty"`
Monitored bool `json:"monitored"`
UseSceneNumbering bool `json:"useSceneNumbering,omitempty"`
}