From cd5c2c4de1dc86ce1110629c72ab1e3e97936bcd Mon Sep 17 00:00:00 2001 From: David Newhall Date: Fri, 14 Jan 2022 23:42:40 -0800 Subject: [PATCH] Update main Interface with Context-aware methods. (#24) * expand interface * export a couple methods * split sonarr into more files * split readarr into more files * split radarr into more files * split proalarr and add systemstatus * split lidarr into more files * add Lookup back to sonarr * add mocks * add context methods to lidarr * add context methods to prowlarr * add context methods to radarr * add context methods to readarr * add context methods to sonarr * wreck interface * this is not th eonly but * typo fix * fix lint --- Makefile | 10 +- go.mod | 2 +- go.sum | 6 +- http.go | 69 +++--- interface.go | 81 ++++--- lidarr/album.go | 131 ++++++++++ lidarr/artist.go | 101 ++++++++ lidarr/command.go | 48 ++++ lidarr/history.go | 90 +++++++ lidarr/lidarr.go | 447 +++++----------------------------- lidarr/qualityprofile.go | 69 ++++++ lidarr/system.go | 43 ++++ lidarr/tag.go | 66 +++++ lidarr/type.go | 29 --- mocks/apier.go | 130 ++++++++++ prowlarr/prowlarr.go | 32 ++- prowlarr/system.go | 43 ++++ prowlarr/type.go | 61 ++--- radarr/command.go | 48 ++++ radarr/customformat.go | 74 ++++++ radarr/exclusions.go | 74 ++++++ radarr/history.go | 89 +++++++ radarr/importlist.go | 92 +++++++ radarr/movie.go | 120 +++++++++ radarr/qualityprofile.go | 69 ++++++ radarr/radarr.go | 496 ++++---------------------------------- radarr/system.go | 43 ++++ radarr/tag.go | 66 +++++ radarr/type.go | 29 --- readarr/author.go | 54 +++++ readarr/book.go | 121 ++++++++++ readarr/command.go | 47 ++++ readarr/history.go | 86 +++++++ readarr/qualityprofile.go | 66 +++++ readarr/readarr.go | 374 ++++------------------------ readarr/system.go | 40 +++ readarr/tag.go | 63 +++++ readarr/type.go | 29 --- sonarr/command.go | 70 ++++++ sonarr/episode.go | 53 ++++ sonarr/history.go | 85 +++++++ sonarr/qualityprofile.go | 66 +++++ sonarr/releaseprofile.go | 66 +++++ sonarr/series.go | 141 +++++++++++ sonarr/sonarr.go | 475 ++++-------------------------------- sonarr/system.go | 40 +++ sonarr/tag.go | 63 +++++ sonarr/type.go | 67 +---- 48 files changed, 2803 insertions(+), 1861 deletions(-) create mode 100644 lidarr/album.go create mode 100644 lidarr/artist.go create mode 100644 lidarr/command.go create mode 100644 lidarr/history.go create mode 100644 lidarr/qualityprofile.go create mode 100644 lidarr/system.go create mode 100644 lidarr/tag.go create mode 100644 prowlarr/system.go create mode 100644 radarr/command.go create mode 100644 radarr/customformat.go create mode 100644 radarr/exclusions.go create mode 100644 radarr/history.go create mode 100644 radarr/importlist.go create mode 100644 radarr/movie.go create mode 100644 radarr/qualityprofile.go create mode 100644 radarr/system.go create mode 100644 radarr/tag.go create mode 100644 readarr/author.go create mode 100644 readarr/book.go create mode 100644 readarr/command.go create mode 100644 readarr/history.go create mode 100644 readarr/qualityprofile.go create mode 100644 readarr/system.go create mode 100644 readarr/tag.go create mode 100644 sonarr/command.go create mode 100644 sonarr/episode.go create mode 100644 sonarr/history.go create mode 100644 sonarr/qualityprofile.go create mode 100644 sonarr/releaseprofile.go create mode 100644 sonarr/series.go create mode 100644 sonarr/system.go create mode 100644 sonarr/tag.go diff --git a/Makefile b/Makefile index 4cca7c5..ef01951 100644 --- a/Makefile +++ b/Makefile @@ -9,10 +9,10 @@ test: lint nopollution lint: # Test lint on four platforms. - GOOS=linux golangci-lint run --enable-all -D maligned,scopelint,interfacer,golint,tagliatelle,exhaustivestruct - GOOS=darwin golangci-lint run --enable-all -D maligned,scopelint,interfacer,golint,tagliatelle,exhaustivestruct - GOOS=windows golangci-lint run --enable-all -D maligned,scopelint,interfacer,golint,tagliatelle,exhaustivestruct - GOOS=freebsd golangci-lint run --enable-all -D maligned,scopelint,interfacer,golint,tagliatelle,exhaustivestruct + GOOS=linux golangci-lint run --enable-all -D maligned,scopelint,interfacer,golint,tagliatelle,exhaustivestruct,dupl + GOOS=darwin golangci-lint run --enable-all -D maligned,scopelint,interfacer,golint,tagliatelle,exhaustivestruct,dupl + GOOS=windows golangci-lint run --enable-all -D maligned,scopelint,interfacer,golint,tagliatelle,exhaustivestruct,dupl + GOOS=freebsd golangci-lint run --enable-all -D maligned,scopelint,interfacer,golint,tagliatelle,exhaustivestruct,dupl # Some of these are borderline. For instance "edition" shows up in radarr payloads. "series" shows up in Readarr, "author" in Sonarr, etc. # If these catch legitimate uses, just remove the piece that caught it. @@ -22,7 +22,7 @@ nopollution: grep -riE 'readar|sonar|lidar|prowl|series|episode|author|book|artist|album|v1' radarr || exit 0 && exit 1 grep -riE 'radar|sonar|lidar|prowl|episode|movie|artist|album|v3' readarr || exit 0 && exit 1 grep -riE 'readar|radar|lidar|prowl|book|edition|movie|artist|album|v1' sonarr || exit 0 && exit 1 - grep -riE 'readar|radar|lidar|sonar|series|episode|author|book|edition|movie|artist|album|track|v3' prowlarr || exit 0 && exit 1 + grep -riE 'readar|radar|lidar|sonar|series|episode|book|edition|movie|artist|album|track|v3' prowlarr || exit 0 && exit 1 generate: go generate ./... diff --git a/go.mod b/go.mod index 59f5469..0f7af87 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,5 @@ go 1.17 require ( github.com/golang/mock v1.6.0 - golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // publicsuffix, cookiejar. + golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d // publicsuffix, cookiejar. ) diff --git a/go.sum b/go.sum index a735c89..e58c88f 100644 --- a/go.sum +++ b/go.sum @@ -7,18 +7,20 @@ golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM= -golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d h1:1n1fc535VhN8SYtD4cDUyNlfpAF2ROMM9+11equK3hs= +golang.org/x/net v0.0.0-20220114011407-0dd24b26b47d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= diff --git a/http.go b/http.go index a046d81..5ce5931 100644 --- a/http.go +++ b/http.go @@ -15,17 +15,36 @@ import ( /* The methods in this file provide assumption-ridden HTTP calls for Starr apps. */ -// req returns te body in []byte form (already read). -func (c *Config) req(path, method string, params url.Values, body io.Reader) (int, []byte, http.Header, error) { - ctx, cancel := context.WithTimeout(context.Background(), c.Timeout.Duration) - defer cancel() - - req, err := c.newReq(ctx, c.setPathParams(path, params), method, params, body) +// Req makes an http request and returns the body in []byte form (already read). +func (c *Config) Req(ctx context.Context, path, method string, params url.Values, + body io.Reader) (int, []byte, http.Header, error) { + req, err := c.newReq(ctx, c.SetPathParams(path, params), method, params, body) if err != nil { return 0, nil, nil, err } - return c.getBody(req) + resp, err := c.Client.Do(req) + if err != nil { + return 0, nil, nil, fmt.Errorf("httpClient.Do(req): %w", err) + } + defer resp.Body.Close() + + respBody, err := ioutil.ReadAll(resp.Body) + if err != nil { + return resp.StatusCode, nil, resp.Header, fmt.Errorf("ioutil.ReadAll: %w", err) + } + + // ############################################# + // DEBUG: useful for viewing payloads from apps. + // log.Println(resp.StatusCode, resp.Header.Get("location"), string(body)) + // ############################################# + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return resp.StatusCode, respBody, resp.Header, fmt.Errorf("failed: %v (status: %s): %w: %s", + resp.Request.RequestURI, resp.Status, ErrInvalidStatusCode, string(respBody)) + } + + return resp.StatusCode, respBody, resp.Header, nil } // body returns the body in io.ReadCloser form (read and close it yourself). @@ -55,14 +74,14 @@ func (c *Config) newReq(ctx context.Context, path, method string, return nil, fmt.Errorf("http.NewRequestWithContext(path): %w", err) } - c.setHeaders(req) + c.SetHeaders(req) req.URL.RawQuery = params.Encode() return req, nil } -// setHeaders sets all our request headers. -func (c *Config) setHeaders(req *http.Request) { +// SetHeaders sets all our request headers based on method and other data. +func (c *Config) SetHeaders(req *http.Request) { // This app allows http auth, in addition to api key (nginx proxy). if auth := c.HTTPUser + ":" + c.HTTPPass; auth != ":" { req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte(auth))) @@ -82,36 +101,10 @@ func (c *Config) setHeaders(req *http.Request) { req.Header.Set("X-API-Key", c.APIKey) } -// getBody makes an http request and returns the response body if there are no errors. -func (c *Config) getBody(req *http.Request) (int, []byte, http.Header, error) { - resp, err := c.Client.Do(req) - if err != nil { - return 0, nil, nil, fmt.Errorf("httpClient.Do(req): %w", err) - } - defer resp.Body.Close() - - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - return resp.StatusCode, nil, resp.Header, fmt.Errorf("ioutil.ReadAll: %w", err) - } - - // ############################################# - // DEBUG: useful for viewing payloads from apps. - // log.Println(resp.StatusCode, resp.Header.Get("location"), string(body)) - // ############################################# - - if resp.StatusCode < 200 || resp.StatusCode > 299 { - return resp.StatusCode, body, resp.Header, fmt.Errorf("failed: %v (status: %s): %w: %s", - resp.Request.RequestURI, resp.Status, ErrInvalidStatusCode, string(body)) - } - - return resp.StatusCode, body, resp.Header, nil -} - -// setPathParams makes sure the path starts with /api and returns the full URL. +// SetPathParams makes sure the path starts with /api and returns the full URL. // Also makes sure params is not nil (so it can be encoded later). // Sets the apikey as a path parameter for use by older radarr/sonarr versions. -func (c *Config) setPathParams(uriPath string, params url.Values) string { +func (c *Config) SetPathParams(uriPath string, params url.Values) string { if strings.Contains(uriPath, "api/") { uriPath = path.Join("/", uriPath) } else { diff --git a/interface.go b/interface.go index d1dce2e..b938f63 100644 --- a/interface.go +++ b/interface.go @@ -20,15 +20,18 @@ import ( // APIer is used by the sub packages to allow mocking the http methods in tests. // This also allows consuming packages to override methods. type APIer interface { - Login() error // Only needed for non-API paths, like backup downloads. Requires Username and Password being set. - Get(path string, params url.Values) (respBody []byte, err error) - Post(path string, params url.Values, postBody []byte) (respBody []byte, err error) - Put(path string, params url.Values, putBody []byte) (respBody []byte, err error) - Delete(path string, params url.Values) (respBody []byte, err error) - GetInto(path string, params url.Values, v interface{}) error - PostInto(path string, params url.Values, postBody []byte, v interface{}) error - PutInto(path string, params url.Values, putBody []byte, v interface{}) error - DeleteInto(path string, params url.Values, v interface{}) error + Login(ctx context.Context) error + // Normal data, returns http body. + Get(ctx context.Context, path string, params url.Values) (respBody []byte, err error) + Post(ctx context.Context, path string, params url.Values, postBody []byte) (respBody []byte, err error) + Put(ctx context.Context, path string, params url.Values, putBody []byte) (respBody []byte, err error) + Delete(ctx context.Context, path string, params url.Values) (respBody []byte, err error) + // Normal data, unmarshals into provided interface. + GetInto(ctx context.Context, path string, params url.Values, v interface{}) error + PostInto(ctx context.Context, path string, params url.Values, postBody []byte, v interface{}) error + PutInto(ctx context.Context, path string, params url.Values, putBody []byte, v interface{}) error + DeleteInto(ctx context.Context, path string, params url.Values, v interface{}) error + // Body methods. GetBody(ctx context.Context, path string, params url.Values) (respBody io.ReadCloser, status int, err error) PostBody(ctx context.Context, path string, params url.Values, postBody []byte) (respBody io.ReadCloser, status int, err error) @@ -68,8 +71,8 @@ func (c *Config) log(code int, data, body []byte, header http.Header, path, meth } } -// Login POSTs to the login form in a Starr app and saves the authentication cookie for future use. -func (c *Config) Login() error { +// LoginC POSTs to the login form in a Starr app and saves the authentication cookie for future use. +func (c *Config) Login(ctx context.Context) error { if c.Client.Jar == nil { jar, err := cookiejar.New(&cookiejar.Options{PublicSuffixList: publicsuffix.List}) if err != nil { @@ -81,7 +84,7 @@ func (c *Config) Login() error { post := []byte("username=" + c.Username + "&password=" + c.Password) - code, resp, header, err := c.body(context.Background(), "/login", http.MethodPost, nil, bytes.NewBuffer(post)) + code, resp, header, err := c.body(ctx, "/login", http.MethodPost, nil, bytes.NewBuffer(post)) c.log(code, nil, post, header, c.URL+"/login", http.MethodPost, err) if err != nil { @@ -102,61 +105,67 @@ func (c *Config) Login() error { } // Get makes a GET http request and returns the body. -func (c *Config) Get(path string, params url.Values) ([]byte, error) { - code, data, header, err := c.req(path, http.MethodGet, params, nil) - c.log(code, data, nil, header, c.setPathParams(path, params), http.MethodGet, err) +func (c *Config) Get(ctx context.Context, path string, params url.Values) ([]byte, error) { + code, data, header, err := c.Req(ctx, path, http.MethodGet, params, nil) + c.log(code, data, nil, header, c.SetPathParams(path, params), http.MethodGet, err) return data, err } // Post makes a POST http request and returns the body. -func (c *Config) Post(path string, params url.Values, postBody []byte) ([]byte, error) { - code, data, header, err := c.req(path, http.MethodPost, params, bytes.NewBuffer(postBody)) - c.log(code, data, postBody, header, c.setPathParams(path, params), http.MethodPost, err) +func (c *Config) Post(ctx context.Context, path string, params url.Values, postBody []byte) ([]byte, error) { + code, data, header, err := c.Req(ctx, path, http.MethodPost, params, bytes.NewBuffer(postBody)) + c.log(code, data, postBody, header, c.SetPathParams(path, params), http.MethodPost, err) return data, err } // Put makes a PUT http request and returns the body. -func (c *Config) Put(path string, params url.Values, putBody []byte) ([]byte, error) { - code, data, header, err := c.req(path, http.MethodPut, params, bytes.NewBuffer(putBody)) - c.log(code, data, putBody, header, c.setPathParams(path, params), http.MethodPut, err) +func (c *Config) Put(ctx context.Context, path string, params url.Values, putBody []byte) ([]byte, error) { + code, data, header, err := c.Req(ctx, path, http.MethodPut, params, bytes.NewBuffer(putBody)) + c.log(code, data, putBody, header, c.SetPathParams(path, params), http.MethodPut, err) return data, err } // Delete makes a DELETE http request and returns the body. -func (c *Config) Delete(path string, params url.Values) ([]byte, error) { - code, data, header, err := c.req(path, http.MethodDelete, params, nil) - c.log(code, data, nil, header, c.setPathParams(path, params), http.MethodDelete, err) +func (c *Config) Delete(ctx context.Context, path string, params url.Values) ([]byte, error) { + code, data, header, err := c.Req(ctx, path, http.MethodDelete, params, nil) + c.log(code, data, nil, header, c.SetPathParams(path, params), http.MethodDelete, err) return data, err } -// GetInto performs an HTTP GET against an API path and unmarshals the payload into the provided pointer interface. -func (c *Config) GetInto(path string, params url.Values, v interface{}) error { - data, err := c.Get(path, params) +// GetInto performs an HTTP GET against an API path and +// unmarshals the payload into the provided pointer interface. +func (c *Config) GetInto(ctx context.Context, path string, params url.Values, v interface{}) error { + data, err := c.Get(ctx, path, params) return unmarshal(v, data, err) } -// PostInto performs an HTTP POST against an API path and unmarshals the payload into the provided pointer interface. -func (c *Config) PostInto(path string, params url.Values, postBody []byte, v interface{}) error { - data, err := c.Post(path, params, postBody) +// PostInto performs an HTTP POST against an API path and +// unmarshals the payload into the provided pointer interface. +func (c *Config) PostInto(ctx context.Context, path string, + params url.Values, postBody []byte, v interface{}) error { + data, err := c.Post(ctx, path, params, postBody) return unmarshal(v, data, err) } -// PutInto performs an HTTP PUT against an API path and unmarshals the payload into the provided pointer interface. -func (c *Config) PutInto(path string, params url.Values, putBody []byte, v interface{}) error { - data, err := c.Put(path, params, putBody) +// PutInto performs an HTTP PUT against an API path and +// unmarshals the payload into the provided pointer interface. +func (c *Config) PutInto(ctx context.Context, path string, + params url.Values, putBody []byte, v interface{}) error { + data, err := c.Put(ctx, path, params, putBody) return unmarshal(v, data, err) } -// DeleteInto performs an HTTP DELETE against an API path and unmarshals the payload into a pointer interface. -func (c *Config) DeleteInto(path string, params url.Values, v interface{}) error { - data, err := c.Delete(path, params) +// DeleteInto performs an HTTP DELETE against an API path +// and unmarshals the payload into a pointer interface. +func (c *Config) DeleteInto(ctx context.Context, path string, params url.Values, v interface{}) error { + data, err := c.Delete(ctx, path, params) return unmarshal(v, data, err) } diff --git a/lidarr/album.go b/lidarr/album.go new file mode 100644 index 0000000..66b9741 --- /dev/null +++ b/lidarr/album.go @@ -0,0 +1,131 @@ +package lidarr + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "strconv" + + "golift.io/starr" +) + +// GetAlbum returns an album or all albums if mbID is "" (empty). +// mbID is the music brainz UUID for a "release-group". +func (l *Lidarr) GetAlbum(mbID string) ([]*Album, error) { + return l.GetAlbumContext(context.Background(), mbID) +} + +// GetAlbumContext returns an album or all albums if mbID is "" (empty). +// mbID is the music brainz UUID for a "release-group". +func (l *Lidarr) GetAlbumContext(ctx context.Context, mbID string) ([]*Album, error) { + params := make(url.Values) + + if mbID != "" { + params.Add("ForeignAlbumId", mbID) + } + + var albums []*Album + + err := l.GetInto(ctx, "v1/album", params, &albums) + if err != nil { + return nil, fmt.Errorf("api.Get(album): %w", err) + } + + return albums, nil +} + +// GetAlbumByID returns an album by DB ID. +func (l *Lidarr) GetAlbumByID(albumID int64) (*Album, error) { + return l.GetAlbumByIDContext(context.Background(), albumID) +} + +// GetAlbumByIDContext returns an album by DB ID. +func (l *Lidarr) GetAlbumByIDContext(ctx context.Context, albumID int64) (*Album, error) { + var album Album + + err := l.GetInto(ctx, "v1/album/"+strconv.FormatInt(albumID, starr.Base10), nil, &album) + if err != nil { + return nil, fmt.Errorf("api.Get(album): %w", err) + } + + return &album, nil +} + +// UpdateAlbum updates an album in place; the output of this is currently unknown!!!! +func (l *Lidarr) UpdateAlbum(albumID int64, album *Album) (*Album, error) { + return l.UpdateAlbumContext(context.Background(), albumID, album) +} + +// UpdateAlbumContext updates an album in place; the output of this is currently unknown!!!! +func (l *Lidarr) UpdateAlbumContext(ctx context.Context, albumID int64, album *Album) (*Album, error) { + put, err := json.Marshal(album) + if err != nil { + return nil, fmt.Errorf("json.Marshal(album): %w", err) + } + + params := make(url.Values) + params.Add("moveFiles", "true") + + var output Album + + err = l.PutInto(ctx, "v1/album/"+strconv.FormatInt(albumID, starr.Base10), params, put, &output) + if err != nil { + return nil, fmt.Errorf("api.Put(album): %w", err) + } + + return &output, nil +} + +// AddAlbum adds a new album to Lidarr, and probably does not yet work. +func (l *Lidarr) AddAlbum(album *AddAlbumInput) (*Album, error) { + return l.AddAlbumContext(context.Background(), album) +} + +// AddAlbumContext adds a new album to Lidarr, and probably does not yet work. +func (l *Lidarr) AddAlbumContext(ctx context.Context, album *AddAlbumInput) (*Album, error) { + if album.Releases == nil { + album.Releases = make([]*AddAlbumInputRelease, 0) + } + + body, err := json.Marshal(album) + if err != nil { + return nil, fmt.Errorf("json.Marshal(album): %w", err) + } + + params := make(url.Values) + params.Add("moveFiles", "true") + + var output Album + + err = l.PostInto(ctx, "v1/album", params, body, &output) + if err != nil { + return nil, fmt.Errorf("api.Post(album): %w", err) + } + + return &output, nil +} + +// Lookup will search for albums matching the specified search term. +func (l *Lidarr) Lookup(term string) ([]*Album, error) { + return l.LookupContext(context.Background(), term) +} + +// LookupContext will search for albums matching the specified search term. +func (l *Lidarr) LookupContext(ctx context.Context, term string) ([]*Album, error) { + var output []*Album + + if term == "" { + return output, nil + } + + params := make(url.Values) + params.Set("term", term) + + err := l.GetInto(ctx, "v1/album/lookup", params, &output) + if err != nil { + return nil, fmt.Errorf("api.Get(album/lookup): %w", err) + } + + return output, nil +} diff --git a/lidarr/artist.go b/lidarr/artist.go new file mode 100644 index 0000000..a186296 --- /dev/null +++ b/lidarr/artist.go @@ -0,0 +1,101 @@ +package lidarr + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "strconv" + + "golift.io/starr" +) + +// GetArtist returns an artist or all artists. +func (l *Lidarr) GetArtist(mbID string) ([]*Artist, error) { + return l.GetArtistContext(context.Background(), mbID) +} + +// GetArtistContext returns an artist or all artists. +func (l *Lidarr) GetArtistContext(ctx context.Context, mbID string) ([]*Artist, error) { + params := make(url.Values) + + if mbID != "" { + params.Add("mbId", mbID) + } + + var artist []*Artist + + err := l.GetInto(ctx, "v1/artist", params, &artist) + if err != nil { + return artist, fmt.Errorf("api.Get(artist): %w", err) + } + + return artist, nil +} + +// GetArtistByID returns an artist from an ID. +func (l *Lidarr) GetArtistByID(artistID int64) (*Artist, error) { + return l.GetArtistByIDContext(context.Background(), artistID) +} + +// GetArtistByIDContext returns an artist from an ID. +func (l *Lidarr) GetArtistByIDContext(ctx context.Context, artistID int64) (*Artist, error) { + var artist Artist + + err := l.GetInto(ctx, "v1/artist/"+strconv.FormatInt(artistID, starr.Base10), nil, &artist) + if err != nil { + return &artist, fmt.Errorf("api.Get(artist): %w", err) + } + + return &artist, nil +} + +// AddArtist adds a new artist to Lidarr, and probably does not yet work. +func (l *Lidarr) AddArtist(artist *Artist) (*Artist, error) { + return l.AddArtistContext(context.Background(), artist) +} + +// AddArtistContext adds a new artist to Lidarr, and probably does not yet work. +func (l *Lidarr) AddArtistContext(ctx context.Context, artist *Artist) (*Artist, error) { + body, err := json.Marshal(artist) + if err != nil { + return nil, fmt.Errorf("json.Marshal(album): %w", err) + } + + params := make(url.Values) + params.Add("moveFiles", "true") + + var output Artist + + err = l.PostInto(ctx, "v1/artist", params, body, &output) + if err != nil { + return nil, fmt.Errorf("api.Post(artist): %w", err) + } + + return &output, nil +} + +// UpdateArtist updates an artist in place. +func (l *Lidarr) UpdateArtist(artist *Artist) (*Artist, error) { + return l.UpdateArtistContext(context.Background(), artist) +} + +// UpdateArtistContext updates an artist in place. +func (l *Lidarr) UpdateArtistContext(ctx context.Context, artist *Artist) (*Artist, error) { + body, err := json.Marshal(artist) + if err != nil { + return nil, fmt.Errorf("json.Marshal(album): %w", err) + } + + params := make(url.Values) + params.Add("moveFiles", "true") + + var output Artist + + err = l.PutInto(ctx, "v1/artist/"+strconv.FormatInt(artist.ID, starr.Base10), params, body, &output) + if err != nil { + return nil, fmt.Errorf("api.Put(artist): %w", err) + } + + return &output, nil +} diff --git a/lidarr/command.go b/lidarr/command.go new file mode 100644 index 0000000..3f358fd --- /dev/null +++ b/lidarr/command.go @@ -0,0 +1,48 @@ +package lidarr + +import ( + "context" + "encoding/json" + "fmt" +) + +// GetCommands returns all available Lidarr commands. +func (l *Lidarr) GetCommands() ([]*CommandResponse, error) { + return l.GetCommandsContext(context.Background()) +} + +// GetCommandsContext returns all available Lidarr commands. +func (l *Lidarr) GetCommandsContext(ctx context.Context) ([]*CommandResponse, error) { + var output []*CommandResponse + + if err := l.GetInto(ctx, "v1/command", nil, &output); err != nil { + return nil, fmt.Errorf("api.Get(command): %w", err) + } + + return output, nil +} + +// SendCommand sends a command to Lidarr. +func (l *Lidarr) SendCommand(cmd *CommandRequest) (*CommandResponse, error) { + return l.SendCommandContext(context.Background(), cmd) +} + +// SendCommandContext sends a command to Lidarr. +func (l *Lidarr) SendCommandContext(ctx context.Context, cmd *CommandRequest) (*CommandResponse, error) { + var output CommandResponse + + if cmd == nil || cmd.Name == "" { + return &output, nil + } + + body, err := json.Marshal(cmd) + if err != nil { + return nil, fmt.Errorf("json.Marshal(cmd): %w", err) + } + + if err := l.PostInto(ctx, "v1/command", nil, body, &output); err != nil { + return nil, fmt.Errorf("api.Post(command): %w", err) + } + + return &output, nil +} diff --git a/lidarr/history.go b/lidarr/history.go new file mode 100644 index 0000000..55f6f79 --- /dev/null +++ b/lidarr/history.go @@ -0,0 +1,90 @@ +package lidarr + +import ( + "context" + "fmt" + "strconv" + + "golift.io/starr" +) + +// GetHistory returns the Lidarr History (grabs/failures/completed). +// WARNING: 12/30/2021 - this method changed. +// If you need control over the page, use lidarr.GetHistoryPage(). +// This function simply returns the number of history records desired, +// up to the number of records present in the application. +// It grabs records in (paginated) batches of perPage, and concatenates +// them into one list. Passing zero for records will return all of them. +func (l *Lidarr) GetHistory(records, perPage int) (*History, error) { + return l.GetHistoryContext(context.Background(), records, perPage) +} + +// GetHistoryContext returns the Lidarr History (grabs/failures/completed). +func (l *Lidarr) GetHistoryContext(ctx context.Context, records, perPage int) (*History, error) { + hist := &History{Records: []*HistoryRecord{}} + perPage = starr.SetPerPage(records, perPage) + + for page := 1; ; page++ { + curr, err := l.GetHistoryPageContext(ctx, &starr.Req{PageSize: perPage, Page: page}) + if err != nil { + return nil, err + } + + hist.Records = append(hist.Records, curr.Records...) + + if len(hist.Records) >= curr.TotalRecords || + (len(hist.Records) >= records && records != 0) || + len(curr.Records) == 0 { + hist.PageSize = curr.TotalRecords + hist.TotalRecords = curr.TotalRecords + hist.SortDirection = curr.SortDirection + hist.SortKey = curr.SortKey + + break + } + + perPage = starr.AdjustPerPage(records, curr.TotalRecords, len(hist.Records), perPage) + } + + return hist, nil +} + +// GetHistoryPage returns a single page from the Lidarr History (grabs/failures/completed). +// The page size and number is configurable with the input request parameters. +func (l *Lidarr) GetHistoryPage(params *starr.Req) (*History, error) { + return l.GetHistoryPageContext(context.Background(), params) +} + +// GetHistoryPageContext returns a single page from the Lidarr History (grabs/failures/completed). +// The page size and number is configurable with the input request parameters. +func (l *Lidarr) GetHistoryPageContext(ctx context.Context, params *starr.Req) (*History, error) { + var history History + + err := l.GetInto(ctx, "v1/history", params.Params(), &history) + if err != nil { + return nil, fmt.Errorf("api.Get(history): %w", err) + } + + return &history, nil +} + +// Fail marks the given history item as failed by id. +func (l *Lidarr) Fail(historyID int64) error { + return l.FailContext(context.Background(), historyID) +} + +// FailContext marks the given history item as failed by id. +func (l *Lidarr) FailContext(ctx context.Context, historyID int64) error { + if historyID < 1 { + return fmt.Errorf("%w: invalid history ID: %d", starr.ErrRequestError, historyID) + } + + post := []byte("id=" + strconv.FormatInt(historyID, starr.Base10)) + + _, err := l.Post(ctx, "v1/history/failed", nil, post) + if err != nil { + return fmt.Errorf("api.Post(history/failed): %w", err) + } + + return nil +} diff --git a/lidarr/lidarr.go b/lidarr/lidarr.go index 518ed85..46f3b57 100644 --- a/lidarr/lidarr.go +++ b/lidarr/lidarr.go @@ -1,92 +1,39 @@ package lidarr import ( - "encoding/json" + "context" + "crypto/tls" "fmt" - "net/url" - "strconv" + "net/http" "golift.io/starr" ) -// GetQualityDefinition returns the Quality Definitions. -func (l *Lidarr) GetQualityDefinition() ([]*QualityDefinition, error) { - var definition []*QualityDefinition - - err := l.GetInto("v1/qualitydefinition", nil, &definition) - if err != nil { - return nil, fmt.Errorf("api.Get(qualitydefinition): %w", err) - } - - return definition, nil -} - -// GetQualityProfiles returns the quality profiles. -func (l *Lidarr) GetQualityProfiles() ([]*QualityProfile, error) { - var profiles []*QualityProfile - - err := l.GetInto("v1/qualityprofile", nil, &profiles) - if err != nil { - return nil, fmt.Errorf("api.Get(qualityprofile): %w", err) - } - - return profiles, nil -} - -// AddQualityProfile updates a quality profile in place. -func (l *Lidarr) AddQualityProfile(profile *QualityProfile) (int64, error) { - post, err := json.Marshal(profile) - if err != nil { - return 0, fmt.Errorf("json.Marshal(profile): %w", err) - } - - var output QualityProfile - - err = l.PostInto("v1/qualityProfile", nil, post, &output) - if err != nil { - return 0, fmt.Errorf("api.Post(qualityProfile): %w", err) - } - - return output.ID, nil -} - -// UpdateQualityProfile updates a quality profile in place. -func (l *Lidarr) UpdateQualityProfile(profile *QualityProfile) error { - put, err := json.Marshal(profile) - if err != nil { - return fmt.Errorf("json.Marshal(profile): %w", err) - } - - _, err = l.Put("v1/qualityProfile/"+strconv.FormatInt(profile.ID, starr.Base10), nil, put) - if err != nil { - return fmt.Errorf("api.Put(qualityProfile): %w", err) - } - - return nil -} - -// GetRootFolders returns all configured root folders. -func (l *Lidarr) GetRootFolders() ([]*RootFolder, error) { - var folders []*RootFolder - - err := l.GetInto("v1/rootFolder", nil, &folders) - if err != nil { - return nil, fmt.Errorf("api.Get(rootFolder): %w", err) +// Lidarr contains all the methods to interact with a Lidarr server. +type Lidarr struct { + starr.APIer +} + +// New returns a Lidarr object used to interact with the Lidarr API. +func New(config *starr.Config) *Lidarr { + if config.Client == nil { + //nolint:exhaustivestruct,gosec + config.Client = &http.Client{ + Timeout: config.Timeout.Duration, + CheckRedirect: func(r *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: !config.ValidSSL}, + }, + } } - return folders, nil -} - -// GetMetadataProfiles returns the metadata profiles. -func (l *Lidarr) GetMetadataProfiles() ([]*MetadataProfile, error) { - var profiles []*MetadataProfile - - err := l.GetInto("v1/metadataprofile", nil, &profiles) - if err != nil { - return nil, fmt.Errorf("api.Get(metadataprofile): %w", err) + if config.Debugf == nil { + config.Debugf = func(string, ...interface{}) {} } - return profiles, nil + return &Lidarr{APIer: config} } // GetQueue returns a single page from the Lidarr Queue (processing, but not yet imported). @@ -96,12 +43,17 @@ func (l *Lidarr) GetMetadataProfiles() ([]*MetadataProfile, error) { // up to the number of records present in the application. // It grabs records in (paginated) batches of perPage, and concatenates // them into one list. Passing zero for records will return all of them. -func (l *Lidarr) GetQueue(records, perPage int) (*Queue, error) { //nolint:dupl +func (l *Lidarr) GetQueue(records, perPage int) (*Queue, error) { + return l.GetQueueContext(context.Background(), records, perPage) +} + +// GetQueueContext returns a single page from the Lidarr Queue (processing, but not yet imported). +func (l *Lidarr) GetQueueContext(ctx context.Context, records, perPage int) (*Queue, error) { queue := &Queue{Records: []*QueueRecord{}} perPage = starr.SetPerPage(records, perPage) for page := 1; ; page++ { - curr, err := l.GetQueuePage(&starr.Req{PageSize: perPage, Page: page}) + curr, err := l.GetQueuePageContext(ctx, &starr.Req{PageSize: perPage, Page: page}) if err != nil { return nil, err } @@ -128,12 +80,18 @@ func (l *Lidarr) GetQueue(records, perPage int) (*Queue, error) { //nolint:dupl // GetQueuePage returns a single page from the Lidarr Queue. // The page size and number is configurable with the input request parameters. func (l *Lidarr) GetQueuePage(params *starr.Req) (*Queue, error) { + return l.GetQueuePageContext(context.Background(), params) +} + +// GetQueuePageContext returns a single page from the Lidarr Queue. +// The page size and number is configurable with the input request parameters. +func (l *Lidarr) GetQueuePageContext(ctx context.Context, params *starr.Req) (*Queue, error) { var queue Queue params.CheckSet("sortKey", "timeleft") params.CheckSet("includeUnknownArtistItems", "true") - err := l.GetInto("v1/queue", params.Params(), &queue) + err := l.GetInto(ctx, "v1/queue", params.Params(), &queue) if err != nil { return nil, fmt.Errorf("api.Get(queue): %w", err) } @@ -141,328 +99,53 @@ func (l *Lidarr) GetQueuePage(params *starr.Req) (*Queue, error) { return &queue, nil } -// GetSystemStatus returns system status. -func (l *Lidarr) GetSystemStatus() (*SystemStatus, error) { - var status SystemStatus - - err := l.GetInto("v1/system/status", nil, &status) - if err != nil { - return nil, fmt.Errorf("api.Get(system/status): %w", err) - } - - return &status, nil -} - -// GetTags returns all the tags. -func (l *Lidarr) GetTags() ([]*starr.Tag, error) { - var tags []*starr.Tag - - err := l.GetInto("v1/tag", nil, &tags) - if err != nil { - return nil, fmt.Errorf("api.Get(tag): %w", err) - } - - return tags, nil -} - -// AddTag adds a tag or returns the ID for an existing tag. -func (l *Lidarr) AddTag(label string) (int, error) { - body, err := json.Marshal(&starr.Tag{Label: label, ID: 0}) - if err != nil { - return 0, fmt.Errorf("json.Marshal(tag): %w", err) - } - - var tag starr.Tag - if err = l.PostInto("v1/tag", nil, body, &tag); err != nil { - return tag.ID, fmt.Errorf("api.Post(tag): %w", err) - } - - return tag.ID, nil -} - -// UpdateTag updates the label for a tag. -func (l *Lidarr) UpdateTag(tagID int, label string) (int, error) { - body, err := json.Marshal(&starr.Tag{Label: label, ID: tagID}) - if err != nil { - return 0, fmt.Errorf("json.Marshal(tag): %w", err) - } - - var tag starr.Tag - if err = l.PutInto("v1/tag", nil, body, &tag); err != nil { - return tag.ID, fmt.Errorf("api.Put(tag): %w", err) - } - - return tag.ID, nil -} - -// GetArtist returns an artist or all artists. -func (l *Lidarr) GetArtist(mbID string) ([]*Artist, error) { - params := make(url.Values) - - if mbID != "" { - params.Add("mbId", mbID) - } - - var artist []*Artist - - err := l.GetInto("v1/artist", params, &artist) - if err != nil { - return artist, fmt.Errorf("api.Get(artist): %w", err) - } - - return artist, nil -} - -// GetArtistByID returns an artist from an ID. -func (l *Lidarr) GetArtistByID(artistID int64) (*Artist, error) { - var artist Artist - - err := l.GetInto("v1/artist/"+strconv.FormatInt(artistID, starr.Base10), nil, &artist) - if err != nil { - return &artist, fmt.Errorf("api.Get(artist): %w", err) - } - - return &artist, nil -} - -// AddArtist adds a new artist to Lidarr, and probably does not yet work. -func (l *Lidarr) AddArtist(artist *Artist) (*Artist, error) { - body, err := json.Marshal(artist) - if err != nil { - return nil, fmt.Errorf("json.Marshal(album): %w", err) - } - - params := make(url.Values) - params.Add("moveFiles", "true") - - var output Artist - - err = l.PostInto("v1/artist", params, body, &output) - if err != nil { - return nil, fmt.Errorf("api.Post(artist): %w", err) - } - - return &output, nil -} - -// UpdateArtist updates an artist in place. -func (l *Lidarr) UpdateArtist(artist *Artist) (*Artist, error) { - body, err := json.Marshal(artist) - if err != nil { - return nil, fmt.Errorf("json.Marshal(album): %w", err) - } - - params := make(url.Values) - params.Add("moveFiles", "true") - - var output Artist - - err = l.PutInto("v1/artist/"+strconv.FormatInt(artist.ID, starr.Base10), params, body, &output) - if err != nil { - return nil, fmt.Errorf("api.Put(artist): %w", err) - } - - return &output, nil -} - -// GetAlbum returns an album or all albums if mbID is "" (empty). -// mbID is the music brainz UUID for a "release-group". -func (l *Lidarr) GetAlbum(mbID string) ([]*Album, error) { - params := make(url.Values) - - if mbID != "" { - params.Add("ForeignAlbumId", mbID) - } - - var albums []*Album - - err := l.GetInto("v1/album", params, &albums) - if err != nil { - return nil, fmt.Errorf("api.Get(album): %w", err) - } - - return albums, nil -} - -// GetAlbumByID returns an album by DB ID. -func (l *Lidarr) GetAlbumByID(albumID int64) (*Album, error) { - var album Album - - err := l.GetInto("v1/album/"+strconv.FormatInt(albumID, starr.Base10), nil, &album) - if err != nil { - return nil, fmt.Errorf("api.Get(album): %w", err) - } - - return &album, nil -} - -// UpdateAlbum updates an album in place; the output of this is currently unknown!!!! -func (l *Lidarr) UpdateAlbum(albumID int64, album *Album) (*Album, error) { - put, err := json.Marshal(album) - if err != nil { - return nil, fmt.Errorf("json.Marshal(album): %w", err) - } - - params := make(url.Values) - params.Add("moveFiles", "true") - - var output Album - - err = l.PutInto("v1/album/"+strconv.FormatInt(albumID, starr.Base10), params, put, &output) - if err != nil { - return nil, fmt.Errorf("api.Put(album): %w", err) - } - - return &output, nil -} - -// AddAlbum adds a new album to Lidarr, and probably does not yet work. -func (l *Lidarr) AddAlbum(album *AddAlbumInput) (*Album, error) { - if album.Releases == nil { - album.Releases = make([]*AddAlbumInputRelease, 0) - } - - body, err := json.Marshal(album) - if err != nil { - return nil, fmt.Errorf("json.Marshal(album): %w", err) - } - - params := make(url.Values) - params.Add("moveFiles", "true") - - var output Album - - err = l.PostInto("v1/album", params, body, &output) - if err != nil { - return nil, fmt.Errorf("api.Post(album): %w", err) - } - - return &output, nil -} - -// GetCommands returns all available Lidarr commands. -func (l *Lidarr) GetCommands() ([]*CommandResponse, error) { - var output []*CommandResponse - - if err := l.GetInto("v1/command", nil, &output); err != nil { - return nil, fmt.Errorf("api.Get(command): %w", err) - } - - return output, nil +// GetQualityDefinition returns the Quality Definitions. +func (l *Lidarr) GetQualityDefinition() ([]*QualityDefinition, error) { + return l.GetQualityDefinitionContext(context.Background()) } -// SendCommand sends a command to Lidarr. -func (l *Lidarr) SendCommand(cmd *CommandRequest) (*CommandResponse, error) { - var output CommandResponse - - if cmd == nil || cmd.Name == "" { - return &output, nil - } +// GetQualityDefinitionContext returns the Quality Definitions. +func (l *Lidarr) GetQualityDefinitionContext(ctx context.Context) ([]*QualityDefinition, error) { + var definition []*QualityDefinition - body, err := json.Marshal(cmd) + err := l.GetInto(ctx, "v1/qualitydefinition", nil, &definition) if err != nil { - return nil, fmt.Errorf("json.Marshal(cmd): %w", err) - } - - if err := l.PostInto("v1/command", nil, body, &output); err != nil { - return nil, fmt.Errorf("api.Post(command): %w", err) + return nil, fmt.Errorf("api.Get(qualitydefinition): %w", err) } - return &output, nil + return definition, nil } -// GetHistory returns the Lidarr History (grabs/failures/completed). -// WARNING: 12/30/2021 - this method changed. -// If you need control over the page, use lidarr.GetHistoryPage(). -// This function simply returns the number of history records desired, -// up to the number of records present in the application. -// It grabs records in (paginated) batches of perPage, and concatenates -// them into one list. Passing zero for records will return all of them. -func (l *Lidarr) GetHistory(records, perPage int) (*History, error) { //nolint:dupl - hist := &History{Records: []*HistoryRecord{}} - perPage = starr.SetPerPage(records, perPage) - - for page := 1; ; page++ { - curr, err := l.GetHistoryPage(&starr.Req{PageSize: perPage, Page: page}) - if err != nil { - return nil, err - } - - hist.Records = append(hist.Records, curr.Records...) - - if len(hist.Records) >= curr.TotalRecords || - (len(hist.Records) >= records && records != 0) || - len(curr.Records) == 0 { - hist.PageSize = curr.TotalRecords - hist.TotalRecords = curr.TotalRecords - hist.SortDirection = curr.SortDirection - hist.SortKey = curr.SortKey - - break - } - - perPage = starr.AdjustPerPage(records, curr.TotalRecords, len(hist.Records), perPage) - } - - return hist, nil +// GetRootFolders returns all configured root folders. +func (l *Lidarr) GetRootFolders() ([]*RootFolder, error) { + return l.GetRootFoldersContext(context.Background()) } -// GetHistoryPage returns a single page from the Lidarr History (grabs/failures/completed). -// The page size and number is configurable with the input request parameters. -func (l *Lidarr) GetHistoryPage(params *starr.Req) (*History, error) { - var history History +// GetRootFoldersContext returns all configured root folders. +func (l *Lidarr) GetRootFoldersContext(ctx context.Context) ([]*RootFolder, error) { + var folders []*RootFolder - err := l.GetInto("v1/history", params.Params(), &history) + err := l.GetInto(ctx, "v1/rootFolder", nil, &folders) if err != nil { - return nil, fmt.Errorf("api.Get(history): %w", err) + return nil, fmt.Errorf("api.Get(rootFolder): %w", err) } - return &history, nil + return folders, nil } -// Fail marks the given history item as failed by id. -func (l *Lidarr) Fail(historyID int64) error { - if historyID < 1 { - return fmt.Errorf("%w: invalid history ID: %d", starr.ErrRequestError, historyID) - } - - post := []byte("id=" + strconv.FormatInt(historyID, starr.Base10)) - - _, err := l.Post("v1/history/failed", nil, post) - if err != nil { - return fmt.Errorf("api.Post(history/failed): %w", err) - } - - return nil +// GetMetadataProfiles returns the metadata profiles. +func (l *Lidarr) GetMetadataProfiles() ([]*MetadataProfile, error) { + return l.GetMetadataProfilesContext(context.Background()) } -// Lookup will search for albums matching the specified search term. -func (l *Lidarr) Lookup(term string) ([]*Album, error) { - var output []*Album - - if term == "" { - return output, nil - } - - params := make(url.Values) - params.Set("term", term) +// GetMetadataProfilesContext returns the metadata profiles. +func (l *Lidarr) GetMetadataProfilesContext(ctx context.Context) ([]*MetadataProfile, error) { + var profiles []*MetadataProfile - err := l.GetInto("v1/album/lookup", params, &output) + err := l.GetInto(ctx, "v1/metadataprofile", nil, &profiles) if err != nil { - return nil, fmt.Errorf("api.Get(album/lookup): %w", err) - } - - return output, nil -} - -// GetBackupFiles returns all available Lidarr backup files. -// Use GetBody to download a file using BackupFile.Path. -func (l *Lidarr) GetBackupFiles() ([]*starr.BackupFile, error) { - var output []*starr.BackupFile - - if err := l.GetInto("v1/system/backup", nil, &output); err != nil { - return nil, fmt.Errorf("api.Get(system/backup): %w", err) + return nil, fmt.Errorf("api.Get(metadataprofile): %w", err) } - return output, nil + return profiles, nil } diff --git a/lidarr/qualityprofile.go b/lidarr/qualityprofile.go new file mode 100644 index 0000000..b0cfe3f --- /dev/null +++ b/lidarr/qualityprofile.go @@ -0,0 +1,69 @@ +package lidarr + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + + "golift.io/starr" +) + +// GetQualityProfiles returns the quality profiles. +func (l *Lidarr) GetQualityProfiles() ([]*QualityProfile, error) { + return l.GetQualityProfilesContext(context.Background()) +} + +// GetQualityProfilesContext returns the quality profiles. +func (l *Lidarr) GetQualityProfilesContext(ctx context.Context) ([]*QualityProfile, error) { + var profiles []*QualityProfile + + err := l.GetInto(ctx, "v1/qualityprofile", nil, &profiles) + if err != nil { + return nil, fmt.Errorf("api.Get(qualityprofile): %w", err) + } + + return profiles, nil +} + +// AddQualityProfile updates a quality profile in place. +func (l *Lidarr) AddQualityProfile(profile *QualityProfile) (int64, error) { + return l.AddQualityProfileContext(context.Background(), profile) +} + +// AddQualityProfileContext updates a quality profile in place. +func (l *Lidarr) AddQualityProfileContext(ctx context.Context, profile *QualityProfile) (int64, error) { + post, err := json.Marshal(profile) + if err != nil { + return 0, fmt.Errorf("json.Marshal(profile): %w", err) + } + + var output QualityProfile + + err = l.PostInto(ctx, "v1/qualityProfile", nil, post, &output) + if err != nil { + return 0, fmt.Errorf("api.Post(qualityProfile): %w", err) + } + + return output.ID, nil +} + +// UpdateQualityProfile updates a quality profile in place. +func (l *Lidarr) UpdateQualityProfile(profile *QualityProfile) error { + return l.UpdateQualityProfileContext(context.Background(), profile) +} + +// UpdateQualityProfileContext updates a quality profile in place. +func (l *Lidarr) UpdateQualityProfileContext(ctx context.Context, profile *QualityProfile) error { + put, err := json.Marshal(profile) + if err != nil { + return fmt.Errorf("json.Marshal(profile): %w", err) + } + + _, err = l.Put(ctx, "v1/qualityProfile/"+strconv.FormatInt(profile.ID, starr.Base10), nil, put) + if err != nil { + return fmt.Errorf("api.Put(qualityProfile): %w", err) + } + + return nil +} diff --git a/lidarr/system.go b/lidarr/system.go new file mode 100644 index 0000000..0623103 --- /dev/null +++ b/lidarr/system.go @@ -0,0 +1,43 @@ +package lidarr + +import ( + "context" + "fmt" + + "golift.io/starr" +) + +// GetSystemStatus returns system status. +func (l *Lidarr) GetSystemStatus() (*SystemStatus, error) { + return l.GetSystemStatusContext(context.Background()) +} + +// GetSystemStatusContext returns system status. +func (l *Lidarr) GetSystemStatusContext(ctx context.Context) (*SystemStatus, error) { + var status SystemStatus + + err := l.GetInto(ctx, "v1/system/status", nil, &status) + if err != nil { + return nil, fmt.Errorf("api.Get(system/status): %w", err) + } + + return &status, nil +} + +// GetBackupFiles returns all available Lidarr backup files. +// Use GetBody to download a file using BackupFile.Path. +func (l *Lidarr) GetBackupFiles() ([]*starr.BackupFile, error) { + return l.GetBackupFilesContext(context.Background()) +} + +// GetBackupFilesContext returns all available Lidarr backup files. +// Use GetBody to download a file using BackupFile.Path. +func (l *Lidarr) GetBackupFilesContext(ctx context.Context) ([]*starr.BackupFile, error) { + var output []*starr.BackupFile + + if err := l.GetInto(ctx, "v1/system/backup", nil, &output); err != nil { + return nil, fmt.Errorf("api.Get(system/backup): %w", err) + } + + return output, nil +} diff --git a/lidarr/tag.go b/lidarr/tag.go new file mode 100644 index 0000000..0a92433 --- /dev/null +++ b/lidarr/tag.go @@ -0,0 +1,66 @@ +package lidarr + +import ( + "context" + "encoding/json" + "fmt" + + "golift.io/starr" +) + +// GetTags returns all the tags. +func (l *Lidarr) GetTags() ([]*starr.Tag, error) { + return l.GetTagsContext(context.Background()) +} + +// GetTagsContext returns all the tags. +func (l *Lidarr) GetTagsContext(ctx context.Context) ([]*starr.Tag, error) { + var tags []*starr.Tag + + err := l.GetInto(ctx, "v1/tag", nil, &tags) + if err != nil { + return nil, fmt.Errorf("api.Get(tag): %w", err) + } + + return tags, nil +} + +// AddTag adds a tag or returns the ID for an existing tag. +func (l *Lidarr) AddTag(label string) (int, error) { + return l.AddTagContext(context.Background(), label) +} + +// AddTagContext adds a tag or returns the ID for an existing tag. +func (l *Lidarr) AddTagContext(ctx context.Context, label string) (int, error) { + body, err := json.Marshal(&starr.Tag{Label: label, ID: 0}) + if err != nil { + return 0, fmt.Errorf("json.Marshal(tag): %w", err) + } + + var tag starr.Tag + if err = l.PostInto(ctx, "v1/tag", nil, body, &tag); err != nil { + return tag.ID, fmt.Errorf("api.Post(tag): %w", err) + } + + return tag.ID, nil +} + +// UpdateTag updates the label for a tag. +func (l *Lidarr) UpdateTag(tagID int, label string) (int, error) { + return l.UpdateTagContext(context.Background(), tagID, label) +} + +// UpdateTagContext updates the label for a tag. +func (l *Lidarr) UpdateTagContext(ctx context.Context, tagID int, label string) (int, error) { + body, err := json.Marshal(&starr.Tag{Label: label, ID: tagID}) + if err != nil { + return 0, fmt.Errorf("json.Marshal(tag): %w", err) + } + + var tag starr.Tag + if err = l.PutInto(ctx, "v1/tag", nil, body, &tag); err != nil { + return tag.ID, fmt.Errorf("api.Put(tag): %w", err) + } + + return tag.ID, nil +} diff --git a/lidarr/type.go b/lidarr/type.go index 3c66579..8249fee 100644 --- a/lidarr/type.go +++ b/lidarr/type.go @@ -1,40 +1,11 @@ package lidarr import ( - "crypto/tls" - "net/http" "time" "golift.io/starr" ) -// Lidarr contains all the methods to interact with a Lidarr server. -type Lidarr struct { - starr.APIer -} - -// New returns a Lidarr object used to interact with the Lidarr API. -func New(config *starr.Config) *Lidarr { - if config.Client == nil { - //nolint:exhaustivestruct,gosec - config.Client = &http.Client{ - Timeout: config.Timeout.Duration, - CheckRedirect: func(r *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - }, - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: !config.ValidSSL}, - }, - } - } - - if config.Debugf == nil { - config.Debugf = func(string, ...interface{}) {} - } - - return &Lidarr{APIer: config} -} - // Queue is the /api/v1/queue endpoint. type Queue struct { Page int `json:"page"` diff --git a/mocks/apier.go b/mocks/apier.go index e834e99..d236ac7 100644 --- a/mocks/apier.go +++ b/mocks/apier.go @@ -67,6 +67,21 @@ func (mr *MockAPIerMockRecorder) DeleteBody(arg0, arg1, arg2 interface{}) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteBody", reflect.TypeOf((*MockAPIer)(nil).DeleteBody), arg0, arg1, arg2) } +// DeleteContext mocks base method. +func (m *MockAPIer) DeleteContext(arg0 context.Context, arg1 string, arg2 url.Values) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteContext", arg0, arg1, arg2) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// DeleteContext indicates an expected call of DeleteContext. +func (mr *MockAPIerMockRecorder) DeleteContext(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteContext", reflect.TypeOf((*MockAPIer)(nil).DeleteContext), arg0, arg1, arg2) +} + // DeleteInto mocks base method. func (m *MockAPIer) DeleteInto(arg0 string, arg1 url.Values, arg2 interface{}) error { m.ctrl.T.Helper() @@ -81,6 +96,20 @@ func (mr *MockAPIerMockRecorder) DeleteInto(arg0, arg1, arg2 interface{}) *gomoc return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteInto", reflect.TypeOf((*MockAPIer)(nil).DeleteInto), arg0, arg1, arg2) } +// DeleteIntoContext mocks base method. +func (m *MockAPIer) DeleteIntoContext(arg0 context.Context, arg1 string, arg2 url.Values, arg3 interface{}) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "DeleteIntoContext", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(error) + return ret0 +} + +// DeleteIntoContext indicates an expected call of DeleteIntoContext. +func (mr *MockAPIerMockRecorder) DeleteIntoContext(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "DeleteIntoContext", reflect.TypeOf((*MockAPIer)(nil).DeleteIntoContext), arg0, arg1, arg2, arg3) +} + // Get mocks base method. func (m *MockAPIer) Get(arg0 string, arg1 url.Values) ([]byte, error) { m.ctrl.T.Helper() @@ -112,6 +141,21 @@ func (mr *MockAPIerMockRecorder) GetBody(arg0, arg1, arg2 interface{}) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBody", reflect.TypeOf((*MockAPIer)(nil).GetBody), arg0, arg1, arg2) } +// GetContext mocks base method. +func (m *MockAPIer) GetContext(arg0 context.Context, arg1 string, arg2 url.Values) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetContext", arg0, arg1, arg2) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetContext indicates an expected call of GetContext. +func (mr *MockAPIerMockRecorder) GetContext(arg0, arg1, arg2 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContext", reflect.TypeOf((*MockAPIer)(nil).GetContext), arg0, arg1, arg2) +} + // GetInto mocks base method. func (m *MockAPIer) GetInto(arg0 string, arg1 url.Values, arg2 interface{}) error { m.ctrl.T.Helper() @@ -126,6 +170,20 @@ func (mr *MockAPIerMockRecorder) GetInto(arg0, arg1, arg2 interface{}) *gomock.C return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetInto", reflect.TypeOf((*MockAPIer)(nil).GetInto), arg0, arg1, arg2) } +// GetIntoContext mocks base method. +func (m *MockAPIer) GetIntoContext(arg0 context.Context, arg1 string, arg2 url.Values, arg3 interface{}) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetIntoContext", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].(error) + return ret0 +} + +// GetIntoContext indicates an expected call of GetIntoContext. +func (mr *MockAPIerMockRecorder) GetIntoContext(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetIntoContext", reflect.TypeOf((*MockAPIer)(nil).GetIntoContext), arg0, arg1, arg2, arg3) +} + // Login mocks base method. func (m *MockAPIer) Login() error { m.ctrl.T.Helper() @@ -140,6 +198,20 @@ func (mr *MockAPIerMockRecorder) Login() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Login", reflect.TypeOf((*MockAPIer)(nil).Login)) } +// LoginContext mocks base method. +func (m *MockAPIer) LoginContext(arg0 context.Context) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "LoginContext", arg0) + ret0, _ := ret[0].(error) + return ret0 +} + +// LoginContext indicates an expected call of LoginContext. +func (mr *MockAPIerMockRecorder) LoginContext(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LoginContext", reflect.TypeOf((*MockAPIer)(nil).LoginContext), arg0) +} + // Post mocks base method. func (m *MockAPIer) Post(arg0 string, arg1 url.Values, arg2 []byte) ([]byte, error) { m.ctrl.T.Helper() @@ -171,6 +243,21 @@ func (mr *MockAPIerMockRecorder) PostBody(arg0, arg1, arg2, arg3 interface{}) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostBody", reflect.TypeOf((*MockAPIer)(nil).PostBody), arg0, arg1, arg2, arg3) } +// PostContext mocks base method. +func (m *MockAPIer) PostContext(arg0 context.Context, arg1 string, arg2 url.Values, arg3 []byte) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PostContext", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PostContext indicates an expected call of PostContext. +func (mr *MockAPIerMockRecorder) PostContext(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostContext", reflect.TypeOf((*MockAPIer)(nil).PostContext), arg0, arg1, arg2, arg3) +} + // PostInto mocks base method. func (m *MockAPIer) PostInto(arg0 string, arg1 url.Values, arg2 []byte, arg3 interface{}) error { m.ctrl.T.Helper() @@ -185,6 +272,20 @@ func (mr *MockAPIerMockRecorder) PostInto(arg0, arg1, arg2, arg3 interface{}) *g return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostInto", reflect.TypeOf((*MockAPIer)(nil).PostInto), arg0, arg1, arg2, arg3) } +// PostIntoContext mocks base method. +func (m *MockAPIer) PostIntoContext(arg0 context.Context, arg1 string, arg2 url.Values, arg3 []byte, arg4 interface{}) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PostIntoContext", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(error) + return ret0 +} + +// PostIntoContext indicates an expected call of PostIntoContext. +func (mr *MockAPIerMockRecorder) PostIntoContext(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PostIntoContext", reflect.TypeOf((*MockAPIer)(nil).PostIntoContext), arg0, arg1, arg2, arg3, arg4) +} + // Put mocks base method. func (m *MockAPIer) Put(arg0 string, arg1 url.Values, arg2 []byte) ([]byte, error) { m.ctrl.T.Helper() @@ -216,6 +317,21 @@ func (mr *MockAPIerMockRecorder) PutBody(arg0, arg1, arg2, arg3 interface{}) *go return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutBody", reflect.TypeOf((*MockAPIer)(nil).PutBody), arg0, arg1, arg2, arg3) } +// PutContext mocks base method. +func (m *MockAPIer) PutContext(arg0 context.Context, arg1 string, arg2 url.Values, arg3 []byte) ([]byte, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PutContext", arg0, arg1, arg2, arg3) + ret0, _ := ret[0].([]byte) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// PutContext indicates an expected call of PutContext. +func (mr *MockAPIerMockRecorder) PutContext(arg0, arg1, arg2, arg3 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutContext", reflect.TypeOf((*MockAPIer)(nil).PutContext), arg0, arg1, arg2, arg3) +} + // PutInto mocks base method. func (m *MockAPIer) PutInto(arg0 string, arg1 url.Values, arg2 []byte, arg3 interface{}) error { m.ctrl.T.Helper() @@ -229,3 +345,17 @@ func (mr *MockAPIerMockRecorder) PutInto(arg0, arg1, arg2, arg3 interface{}) *go mr.mock.ctrl.T.Helper() return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutInto", reflect.TypeOf((*MockAPIer)(nil).PutInto), arg0, arg1, arg2, arg3) } + +// PutIntoContext mocks base method. +func (m *MockAPIer) PutIntoContext(arg0 context.Context, arg1 string, arg2 url.Values, arg3 []byte, arg4 interface{}) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "PutIntoContext", arg0, arg1, arg2, arg3, arg4) + ret0, _ := ret[0].(error) + return ret0 +} + +// PutIntoContext indicates an expected call of PutIntoContext. +func (mr *MockAPIerMockRecorder) PutIntoContext(arg0, arg1, arg2, arg3, arg4 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "PutIntoContext", reflect.TypeOf((*MockAPIer)(nil).PutIntoContext), arg0, arg1, arg2, arg3, arg4) +} diff --git a/prowlarr/prowlarr.go b/prowlarr/prowlarr.go index 2c13492..c22024c 100644 --- a/prowlarr/prowlarr.go +++ b/prowlarr/prowlarr.go @@ -1,19 +1,35 @@ package prowlarr import ( - "fmt" + "crypto/tls" + "net/http" "golift.io/starr" ) -// GetBackupFiles returns all available Prowlarr backup files. -// Use GetBody to download a file using BackupFile.Path. -func (r *Prowlarr) GetBackupFiles() ([]*starr.BackupFile, error) { - var output []*starr.BackupFile +// Prowlarr contains all the methods to interact with a Prowlarr server. +type Prowlarr struct { + starr.APIer +} + +// New returns a Prowlarr object used to interact with the Prowlarr API. +func New(config *starr.Config) *Prowlarr { + if config.Client == nil { + //nolint:exhaustivestruct,gosec + config.Client = &http.Client{ + Timeout: config.Timeout.Duration, + CheckRedirect: func(r *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: !config.ValidSSL}, + }, + } + } - if err := r.GetInto("v1/system/backup", nil, &output); err != nil { - return nil, fmt.Errorf("api.Get(system/backup): %w", err) + if config.Debugf == nil { + config.Debugf = func(string, ...interface{}) {} } - return output, nil + return &Prowlarr{APIer: config} } diff --git a/prowlarr/system.go b/prowlarr/system.go new file mode 100644 index 0000000..b883b7f --- /dev/null +++ b/prowlarr/system.go @@ -0,0 +1,43 @@ +package prowlarr + +import ( + "context" + "fmt" + + "golift.io/starr" +) + +// GetSystemStatus returns system status. +func (p *Prowlarr) GetSystemStatus() (*SystemStatus, error) { + return p.GetSystemStatusContext(context.Background()) +} + +// GetSystemStatusContext returns system status. +func (p *Prowlarr) GetSystemStatusContext(ctx context.Context) (*SystemStatus, error) { + var status SystemStatus + + err := p.GetInto(ctx, "v1/system/status", nil, &status) + if err != nil { + return nil, fmt.Errorf("api.Get(system/status): %w", err) + } + + return &status, nil +} + +// GetBackupFiles returns all available Prowlarr backup files. +// Use GetBody to download a file using BackupFile.Path. +func (p *Prowlarr) GetBackupFiles() ([]*starr.BackupFile, error) { + return p.GetBackupFilesContext(context.Background()) +} + +// GetBackupFiles returns all available Prowlarr backup files. +// Use GetBody to download a file using BackupFile.Path. +func (p *Prowlarr) GetBackupFilesContext(ctx context.Context) ([]*starr.BackupFile, error) { + var output []*starr.BackupFile + + if err := p.GetInto(ctx, "v1/system/backup", nil, &output); err != nil { + return nil, fmt.Errorf("api.Get(system/backup): %w", err) + } + + return output, nil +} diff --git a/prowlarr/type.go b/prowlarr/type.go index c22024c..91302b9 100644 --- a/prowlarr/type.go +++ b/prowlarr/type.go @@ -1,35 +1,38 @@ package prowlarr import ( - "crypto/tls" - "net/http" - - "golift.io/starr" + "time" ) -// Prowlarr contains all the methods to interact with a Prowlarr server. -type Prowlarr struct { - starr.APIer -} - -// New returns a Prowlarr object used to interact with the Prowlarr API. -func New(config *starr.Config) *Prowlarr { - if config.Client == nil { - //nolint:exhaustivestruct,gosec - config.Client = &http.Client{ - Timeout: config.Timeout.Duration, - CheckRedirect: func(r *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - }, - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: !config.ValidSSL}, - }, - } - } - - if config.Debugf == nil { - config.Debugf = func(string, ...interface{}) {} - } - - return &Prowlarr{APIer: config} +// SystemStatus is the /api/v1/system/status endpoint. +type SystemStatus struct { + Version string `json:"version"` + BuildTime time.Time `json:"buildTime"` + IsDebug bool `json:"isDebug"` + IsProduction bool `json:"isProduction"` + IsAdmin bool `json:"isAdmin"` + IsUserInteractive bool `json:"isUserInteractive"` + StartupPath string `json:"startupPath"` + AppData string `json:"appData"` + OsName string `json:"osName"` + OsVersion string `json:"osVersion"` + IsNetCore bool `json:"isNetCore"` + IsMono bool `json:"isMono"` + IsLinux bool `json:"isLinux"` + IsOsx bool `json:"isOsx"` + IsWindows bool `json:"isWindows"` + IsDocker bool `json:"isDocker"` + Mode string `json:"mode"` + Branch string `json:"branch"` + Authentication string `json:"authentication"` + DatabaseType string `json:"databaseType"` + DatabaseVersion string `json:"databaseVersion"` + MigrationVersion int `json:"migrationVersion"` + URLBase string `json:"urlBase"` + RuntimeVersion string `json:"runtimeVersion"` + RuntimeName string `json:"runtimeName"` + StartTime time.Time `json:"startTime"` + PackageVersion string `json:"packageVersion"` + PackageAuthor string `json:"packageAuthor"` + PackageUpdateMechanism string `json:"packageUpdateMechanism"` } diff --git a/radarr/command.go b/radarr/command.go new file mode 100644 index 0000000..069e138 --- /dev/null +++ b/radarr/command.go @@ -0,0 +1,48 @@ +package radarr + +import ( + "context" + "encoding/json" + "fmt" +) + +// GetCommands returns all available Radarr commands. +func (r *Radarr) GetCommands() ([]*CommandResponse, error) { + return r.GetCommandsContext(context.Background()) +} + +// GetCommandsContext returns all available Radarr commands. +func (r *Radarr) GetCommandsContext(ctx context.Context) ([]*CommandResponse, error) { + var output []*CommandResponse + + if err := r.GetInto(ctx, "v3/command", nil, &output); err != nil { + return nil, fmt.Errorf("api.Get(command): %w", err) + } + + return output, nil +} + +// SendCommand sends a command to Radarr. +func (r *Radarr) SendCommand(cmd *CommandRequest) (*CommandResponse, error) { + return r.SendCommandContext(context.Background(), cmd) +} + +// SendCommandContext sends a command to Radarr. +func (r *Radarr) SendCommandContext(ctx context.Context, cmd *CommandRequest) (*CommandResponse, error) { + var output CommandResponse + + if cmd == nil || cmd.Name == "" { + return &output, nil + } + + body, err := json.Marshal(cmd) + if err != nil { + return nil, fmt.Errorf("json.Marshal(cmd): %w", err) + } + + if err := r.PostInto(ctx, "v3/command", nil, body, &output); err != nil { + return nil, fmt.Errorf("api.Post(command): %w", err) + } + + return &output, nil +} diff --git a/radarr/customformat.go b/radarr/customformat.go new file mode 100644 index 0000000..21d7cc1 --- /dev/null +++ b/radarr/customformat.go @@ -0,0 +1,74 @@ +package radarr + +import ( + "context" + "encoding/json" + "fmt" + "strconv" +) + +// GetCustomFormats returns all configured Custom Formats. +func (r *Radarr) GetCustomFormats() ([]*CustomFormat, error) { + return r.GetCustomFormatsContext(context.Background()) +} + +// GetCustomFormatsContext returns all configured Custom Formats. +func (r *Radarr) GetCustomFormatsContext(ctx context.Context) ([]*CustomFormat, error) { + var output []*CustomFormat + if err := r.GetInto(ctx, "v3/customFormat", nil, &output); err != nil { + return nil, fmt.Errorf("api.Get(customFormat): %w", err) + } + + return output, nil +} + +// AddCustomFormat creates a new custom format and returns the response (with ID). +func (r *Radarr) AddCustomFormat(format *CustomFormat) (*CustomFormat, error) { + return r.AddCustomFormatContext(context.Background(), format) +} + +// AddCustomFormatContext creates a new custom format and returns the response (with ID). +func (r *Radarr) AddCustomFormatContext(ctx context.Context, format *CustomFormat) (*CustomFormat, error) { + var output CustomFormat + + if format == nil { + return &output, nil + } + + format.ID = 0 // ID must be zero when adding. + + body, err := json.Marshal(format) + if err != nil { + return nil, fmt.Errorf("json.Marshal(customFormat): %w", err) + } + + if err := r.PostInto(ctx, "v3/customFormat", nil, body, &output); err != nil { + return nil, fmt.Errorf("api.Post(customFormat): %w", err) + } + + return &output, nil +} + +// UpdateCustomFormat updates an existing custom format and returns the response. +func (r *Radarr) UpdateCustomFormat(cf *CustomFormat, cfID int) (*CustomFormat, error) { + return r.UpdateCustomFormatContext(context.Background(), cf, cfID) +} + +// UpdateCustomFormatContext updates an existing custom format and returns the response. +func (r *Radarr) UpdateCustomFormatContext(ctx context.Context, cf *CustomFormat, cfID int) (*CustomFormat, error) { + if cfID == 0 { + cfID = cf.ID + } + + body, err := json.Marshal(cf) + if err != nil { + return nil, fmt.Errorf("json.Marshal(customFormat): %w", err) + } + + var output CustomFormat + if err := r.PutInto(ctx, "v3/customFormat/"+strconv.Itoa(cfID), nil, body, &output); err != nil { + return nil, fmt.Errorf("api.Put(customFormat): %w", err) + } + + return &output, nil +} diff --git a/radarr/exclusions.go b/radarr/exclusions.go new file mode 100644 index 0000000..432a0f7 --- /dev/null +++ b/radarr/exclusions.go @@ -0,0 +1,74 @@ +package radarr + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + + "golift.io/starr" +) + +// GetExclusions returns all configured exclusions from Radarr. +func (r *Radarr) GetExclusions() ([]*Exclusion, error) { + return r.GetExclusionsContext(context.Background()) +} + +// GetExclusionsContext returns all configured exclusions from Radarr. +func (r *Radarr) GetExclusionsContext(ctx context.Context) ([]*Exclusion, error) { + var exclusions []*Exclusion + + err := r.GetInto(ctx, "v3/exclusions", nil, &exclusions) + if err != nil { + return nil, fmt.Errorf("api.Get(exclusions): %w", err) + } + + return exclusions, nil +} + +// DeleteExclusions removes exclusions from Radarr. +func (r *Radarr) DeleteExclusions(ids []int64) error { + return r.DeleteExclusionsContext(context.Background(), ids) +} + +// DeleteExclusionsContext removes exclusions from Radarr. +func (r *Radarr) DeleteExclusionsContext(ctx context.Context, ids []int64) error { + var errs string + + for _, id := range ids { + _, err := r.Delete(ctx, "v3/exclusions/"+strconv.FormatInt(id, starr.Base10), nil) + if err != nil { + errs += err.Error() + " " + } + } + + if errs != "" { + return fmt.Errorf("%w: %s", starr.ErrRequestError, errs) + } + + return nil +} + +// AddExclusions adds an exclusion to Radarr. +func (r *Radarr) AddExclusions(exclusions []*Exclusion) error { + return r.AddExclusionsContext(context.Background(), exclusions) +} + +// AddExclusionsContext adds an exclusion to Radarr. +func (r *Radarr) AddExclusionsContext(ctx context.Context, exclusions []*Exclusion) error { + for i := range exclusions { + exclusions[i].ID = 0 + } + + body, err := json.Marshal(exclusions) + if err != nil { + return fmt.Errorf("json.Marshal(movie): %w", err) + } + + _, err = r.Post(ctx, "v3/exclusions/bulk", nil, body) + if err != nil { + return fmt.Errorf("api.Post(exclusions): %w", err) + } + + return nil +} diff --git a/radarr/history.go b/radarr/history.go new file mode 100644 index 0000000..17ff685 --- /dev/null +++ b/radarr/history.go @@ -0,0 +1,89 @@ +package radarr + +import ( + "context" + "fmt" + "strconv" + + "golift.io/starr" +) + +// GetHistory returns the Radarr History (grabs/failures/completed). +// WARNING: 12/30/2021 - this method changed. The second argument no longer +// controls which page is returned, but instead adjusts the pagination size. +// If you need control over the page, use radarr.GetHistoryPage(). +// This function simply returns the number of history records desired, +// up to the number of records present in the application. +// It grabs records in (paginated) batches of perPage, and concatenates +// them into one list. Passing zero for records will return all of them. +func (r *Radarr) GetHistory(records, perPage int) (*History, error) { + return r.GetHistoryContext(context.Background(), records, perPage) +} + +// GetHistoryContext returns the Radarr History (grabs/failures/completed). +func (r *Radarr) GetHistoryContext(ctx context.Context, records, perPage int) (*History, error) { + hist := &History{Records: []*HistoryRecord{}} + perPage = starr.SetPerPage(records, perPage) + + for page := 1; ; page++ { + curr, err := r.GetHistoryPageContext(ctx, &starr.Req{PageSize: perPage, Page: page}) + if err != nil { + return nil, err + } + + hist.Records = append(hist.Records, curr.Records...) + if len(hist.Records) >= curr.TotalRecords || + (len(hist.Records) >= records && records != 0) || + len(curr.Records) == 0 { + hist.PageSize = curr.TotalRecords + hist.TotalRecords = curr.TotalRecords + hist.SortDirection = curr.SortDirection + hist.SortKey = curr.SortKey + + break + } + + perPage = starr.AdjustPerPage(records, curr.TotalRecords, len(hist.Records), perPage) + } + + return hist, nil +} + +// GetHistoryPage returns a single page from the Radarr History (grabs/failures/completed). +// The page size and number is configurable with the input request parameters. +func (r *Radarr) GetHistoryPage(params *starr.Req) (*History, error) { + return r.GetHistoryPageContext(context.Background(), params) +} + +// GetHistoryPageContext returns a single page from the Radarr History (grabs/failures/completed). +// The page size and number is configurable with the input request parameters. +func (r *Radarr) GetHistoryPageContext(ctx context.Context, params *starr.Req) (*History, error) { + var history History + + err := r.GetInto(ctx, "v3/history", params.Params(), &history) + if err != nil { + return nil, fmt.Errorf("api.Get(history): %w", err) + } + + return &history, nil +} + +// Fail marks the given history item as failed by id. +func (r *Radarr) Fail(historyID int64) error { + return r.FailContext(context.Background(), historyID) +} + +// FailContext marks the given history item as failed by id. +func (r *Radarr) FailContext(ctx context.Context, historyID int64) error { + if historyID < 1 { + return fmt.Errorf("%w: invalid history ID: %d", starr.ErrRequestError, historyID) + } + + // Strangely uses a POST without a payload. + _, err := r.Post(ctx, "v3/history/failed/"+strconv.FormatInt(historyID, starr.Base10), nil, nil) + if err != nil { + return fmt.Errorf("api.Post(history/failed): %w", err) + } + + return nil +} diff --git a/radarr/importlist.go b/radarr/importlist.go new file mode 100644 index 0000000..40a9673 --- /dev/null +++ b/radarr/importlist.go @@ -0,0 +1,92 @@ +package radarr + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + + "golift.io/starr" +) + +// GetImportLists returns all import lists. +func (r *Radarr) GetImportLists() ([]*ImportList, error) { + return r.GetImportListsContext(context.Background()) +} + +// GetImportListsContext returns all import lists. +func (r *Radarr) GetImportListsContext(ctx context.Context) ([]*ImportList, error) { + var output []*ImportList + if err := r.GetInto(ctx, "v3/importlist", nil, &output); err != nil { + return nil, fmt.Errorf("api.Get(importlist): %w", err) + } + + return output, nil +} + +// CreateImportList creates an import list in Radarr. +func (r *Radarr) CreateImportList(il *ImportList) (*ImportList, error) { + return r.CreateImportListContext(context.Background(), il) +} + +// CreateImportListContext creates an import list in Radarr. +func (r *Radarr) CreateImportListContext(ctx context.Context, il *ImportList) (*ImportList, error) { + il.ID = 0 + + body, err := json.Marshal(il) + if err != nil { + return nil, fmt.Errorf("json.Marshal(importlist): %w", err) + } + + var output ImportList + if err := r.PostInto(ctx, "v3/importlist", nil, body, &output); err != nil { + return nil, fmt.Errorf("api.Post(importlist): %w", err) + } + + return &output, nil +} + +// DeleteImportList removes an import list from Radarr. +func (r *Radarr) DeleteImportList(ids []int64) error { + return r.DeleteImportListContext(context.Background(), ids) +} + +// DeleteImportListContext removes an import list from Radarr. +func (r *Radarr) DeleteImportListContext(ctx context.Context, ids []int64) error { + var errs string + + for _, id := range ids { + _, err := r.Delete(ctx, "v3/importlist/"+strconv.FormatInt(id, starr.Base10), nil) + if err != nil { + errs += fmt.Errorf("api.Delete(importlist): %w", err).Error() + " " + } + } + + if errs != "" { + return fmt.Errorf("%w: %s", starr.ErrRequestError, errs) + } + + return nil +} + +// UpdateImportList updates an existing import list and returns the response. +func (r *Radarr) UpdateImportList(list *ImportList) (*ImportList, error) { + return r.UpdateImportListContext(context.Background(), list) +} + +// UpdateImportListContext updates an existing import list and returns the response. +func (r *Radarr) UpdateImportListContext(ctx context.Context, list *ImportList) (*ImportList, error) { + body, err := json.Marshal(list) + if err != nil { + return nil, fmt.Errorf("json.Marshal(importlist): %w", err) + } + + var output ImportList + + err = r.PutInto(ctx, "v3/importlist/"+strconv.FormatInt(list.ID, starr.Base10), nil, body, &output) + if err != nil { + return nil, fmt.Errorf("api.Put(importlist): %w", err) + } + + return &output, nil +} diff --git a/radarr/movie.go b/radarr/movie.go new file mode 100644 index 0000000..dabf7c7 --- /dev/null +++ b/radarr/movie.go @@ -0,0 +1,120 @@ +package radarr + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "strconv" + + "golift.io/starr" +) + +// GetMovie grabs a movie from the queue, or all movies if tmdbId is 0. +func (r *Radarr) GetMovie(tmdbID int64) ([]*Movie, error) { + return r.GetMovieContext(context.Background(), tmdbID) +} + +// GetMovieContext grabs a movie from the queue, or all movies if tmdbId is 0. +func (r *Radarr) GetMovieContext(ctx context.Context, tmdbID int64) ([]*Movie, error) { + params := make(url.Values) + if tmdbID != 0 { + params.Set("tmdbId", strconv.FormatInt(tmdbID, starr.Base10)) + } + + var movie []*Movie + + err := r.GetInto(ctx, "v3/movie", params, &movie) + if err != nil { + return nil, fmt.Errorf("api.Get(movie): %w", err) + } + + return movie, nil +} + +// GetMovieByID grabs a movie from the database by DB [movie] ID. +func (r *Radarr) GetMovieByID(movieID int64) (*Movie, error) { + return r.GetMovieByIDContext(context.Background(), movieID) +} + +// GetMovieByIDContext grabs a movie from the database by DB [movie] ID. +func (r *Radarr) GetMovieByIDContext(ctx context.Context, movieID int64) (*Movie, error) { + var movie Movie + + err := r.GetInto(ctx, "v3/movie/"+strconv.FormatInt(movieID, starr.Base10), nil, &movie) + if err != nil { + return nil, fmt.Errorf("api.Get(movie): %w", err) + } + + return &movie, nil +} + +// UpdateMovie sends a PUT request to update a movie in place. +func (r *Radarr) UpdateMovie(movieID int64, movie *Movie) error { + return r.UpdateMovieContext(context.Background(), movieID, movie) +} + +// UpdateMovieContext sends a PUT request to update a movie in place. +func (r *Radarr) UpdateMovieContext(ctx context.Context, movieID int64, movie *Movie) error { + put, err := json.Marshal(movie) + if err != nil { + return fmt.Errorf("json.Marshal(movie): %w", err) + } + + params := make(url.Values) + params.Add("moveFiles", "true") + + _, err = r.Put(ctx, "v3/movie/"+strconv.FormatInt(movieID, starr.Base10), params, put) + if err != nil { + return fmt.Errorf("api.Put(movie): %w", err) + } + + return nil +} + +// AddMovie adds a movie to the queue. +func (r *Radarr) AddMovie(movie *AddMovieInput) (*AddMovieOutput, error) { + return r.AddMovieContext(context.Background(), movie) +} + +// AddMovieContext adds a movie to the queue. +func (r *Radarr) AddMovieContext(ctx context.Context, movie *AddMovieInput) (*AddMovieOutput, error) { + body, err := json.Marshal(movie) + if err != nil { + return nil, fmt.Errorf("json.Marshal(movie): %w", err) + } + + params := make(url.Values) + params.Add("moveFiles", "true") + + var output AddMovieOutput + if err := r.PostInto(ctx, "v3/movie", params, body, &output); err != nil { + return nil, fmt.Errorf("api.Post(movie): %w", err) + } + + return &output, nil +} + +// Lookup will search for movies matching the specified search term. +func (r *Radarr) Lookup(term string) ([]*Movie, error) { + return r.LookupContext(context.Background(), term) +} + +// LookupContext will search for movies matching the specified search term. +func (r *Radarr) LookupContext(ctx context.Context, term string) ([]*Movie, error) { + var output []*Movie + + if term == "" { + return output, nil + } + + params := make(url.Values) + params.Set("term", term) + + err := r.GetInto(ctx, "v3/movie/lookup", params, &output) + if err != nil { + return nil, fmt.Errorf("api.Get(movie/lookup): %w", err) + } + + return output, nil +} diff --git a/radarr/qualityprofile.go b/radarr/qualityprofile.go new file mode 100644 index 0000000..1f1496f --- /dev/null +++ b/radarr/qualityprofile.go @@ -0,0 +1,69 @@ +package radarr + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + + "golift.io/starr" +) + +// GetQualityProfiles returns all configured quality profiles. +func (r *Radarr) GetQualityProfiles() ([]*QualityProfile, error) { + return r.GetQualityProfilesContext(context.Background()) +} + +// GetQualityProfilesContext returns all configured quality profiles. +func (r *Radarr) GetQualityProfilesContext(ctx context.Context) ([]*QualityProfile, error) { + var profiles []*QualityProfile + + err := r.GetInto(ctx, "v3/qualityProfile", nil, &profiles) + if err != nil { + return nil, fmt.Errorf("api.Get(qualityProfile): %w", err) + } + + return profiles, nil +} + +// AddQualityProfile updates a quality profile in place. +func (r *Radarr) AddQualityProfile(profile *QualityProfile) (int64, error) { + return r.AddQualityProfileContext(context.Background(), profile) +} + +// AddQualityProfileContext updates a quality profile in place. +func (r *Radarr) AddQualityProfileContext(ctx context.Context, profile *QualityProfile) (int64, error) { + post, err := json.Marshal(profile) + if err != nil { + return 0, fmt.Errorf("json.Marshal(profile): %w", err) + } + + var output QualityProfile + + err = r.PostInto(ctx, "v3/qualityProfile", nil, post, &output) + if err != nil { + return 0, fmt.Errorf("api.Post(qualityProfile): %w", err) + } + + return output.ID, nil +} + +// UpdateQualityProfile updates a quality profile in place. +func (r *Radarr) UpdateQualityProfile(profile *QualityProfile) error { + return r.UpdateQualityProfileContext(context.Background(), profile) +} + +// UpdateQualityProfileContext updates a quality profile in place. +func (r *Radarr) UpdateQualityProfileContext(ctx context.Context, profile *QualityProfile) error { + put, err := json.Marshal(profile) + if err != nil { + return fmt.Errorf("json.Marshal(profile): %w", err) + } + + _, err = r.Put(ctx, "v3/qualityProfile/"+strconv.FormatInt(profile.ID, starr.Base10), nil, put) + if err != nil { + return fmt.Errorf("api.Put(qualityProfile): %w", err) + } + + return nil +} diff --git a/radarr/radarr.go b/radarr/radarr.go index a054493..97d9234 100644 --- a/radarr/radarr.go +++ b/radarr/radarr.go @@ -1,117 +1,39 @@ package radarr -// Radarr v3 structs - import ( - "encoding/json" + "context" + "crypto/tls" "fmt" - "net/url" - "strconv" + "net/http" "golift.io/starr" ) -// GetSystemStatus returns system status. -func (r *Radarr) GetSystemStatus() (*SystemStatus, error) { - var status SystemStatus - - err := r.GetInto("v3/system/status", nil, &status) - if err != nil { - return nil, fmt.Errorf("api.Get(system/status): %w", err) - } - - return &status, nil -} - -// GetTags returns all the tags. -func (r *Radarr) GetTags() ([]*starr.Tag, error) { - var tags []*starr.Tag - - err := r.GetInto("v3/tag", nil, &tags) - if err != nil { - return nil, fmt.Errorf("api.Get(tag): %w", err) - } - - return tags, nil -} - -// UpdateTag updates the label for a tag. -func (r *Radarr) UpdateTag(tagID int, label string) (int, error) { - body, err := json.Marshal(&starr.Tag{Label: label, ID: tagID}) - if err != nil { - return 0, fmt.Errorf("json.Marshal(tag): %w", err) - } - - var tag starr.Tag - if err = r.PutInto("v3/tag", nil, body, &tag); err != nil { - return tag.ID, fmt.Errorf("api.Put(tag): %w", err) - } - - return tag.ID, nil -} - -// AddTag adds a tag or returns the ID for an existing tag. -func (r *Radarr) AddTag(label string) (int, error) { - body, err := json.Marshal(&starr.Tag{Label: label, ID: 0}) - if err != nil { - return 0, fmt.Errorf("json.Marshal(tag): %w", err) - } - - var tag starr.Tag - if err = r.PostInto("v3/tag", nil, body, &tag); err != nil { - return tag.ID, fmt.Errorf("api.Post(tag): %w", err) - } - - return tag.ID, nil -} - -// GetHistory returns the Radarr History (grabs/failures/completed). -// WARNING: 12/30/2021 - this method changed. The second argument no longer -// controls which page is returned, but instead adjusts the pagination size. -// If you need control over the page, use radarr.GetHistoryPage(). -// This function simply returns the number of history records desired, -// up to the number of records present in the application. -// It grabs records in (paginated) batches of perPage, and concatenates -// them into one list. Passing zero for records will return all of them. -func (r *Radarr) GetHistory(records, perPage int) (*History, error) { //nolint:dupl - hist := &History{Records: []*HistoryRecord{}} - perPage = starr.SetPerPage(records, perPage) - - for page := 1; ; page++ { - curr, err := r.GetHistoryPage(&starr.Req{PageSize: perPage, Page: page}) - if err != nil { - return nil, err +// Radarr contains all the methods to interact with a Radarr server. +type Radarr struct { + starr.APIer +} + +// New returns a Radarr object used to interact with the Radarr API. +func New(config *starr.Config) *Radarr { + if config.Client == nil { + //nolint:exhaustivestruct,gosec + config.Client = &http.Client{ + Timeout: config.Timeout.Duration, + CheckRedirect: func(r *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: !config.ValidSSL}, + }, } - - hist.Records = append(hist.Records, curr.Records...) - if len(hist.Records) >= curr.TotalRecords || - (len(hist.Records) >= records && records != 0) || - len(curr.Records) == 0 { - hist.PageSize = curr.TotalRecords - hist.TotalRecords = curr.TotalRecords - hist.SortDirection = curr.SortDirection - hist.SortKey = curr.SortKey - - break - } - - perPage = starr.AdjustPerPage(records, curr.TotalRecords, len(hist.Records), perPage) } - return hist, nil -} - -// GetHistoryPage returns a single page from the Radarr History (grabs/failures/completed). -// The page size and number is configurable with the input request parameters. -func (r *Radarr) GetHistoryPage(params *starr.Req) (*History, error) { - var history History - - err := r.GetInto("v3/history", params.Params(), &history) - if err != nil { - return nil, fmt.Errorf("api.Get(history): %w", err) + if config.Debugf == nil { + config.Debugf = func(string, ...interface{}) {} } - return &history, nil + return &Radarr{APIer: config} } // GetQueue returns a single page from the Radarr Queue (processing, but not yet imported). @@ -122,12 +44,17 @@ func (r *Radarr) GetHistoryPage(params *starr.Req) (*History, error) { // up to the number of records present in the application. // It grabs records in (paginated) batches of perPage, and concatenates // them into one list. Passing zero for records will return all of them. -func (r *Radarr) GetQueue(records, perPage int) (*Queue, error) { //nolint:dupl +func (r *Radarr) GetQueue(records, perPage int) (*Queue, error) { + return r.GetQueueContext(context.Background(), records, perPage) +} + +// GetQueueContext returns a single page from the Radarr Queue (processing, but not yet imported). +func (r *Radarr) GetQueueContext(ctx context.Context, records, perPage int) (*Queue, error) { queue := &Queue{Records: []*QueueRecord{}} perPage = starr.SetPerPage(records, perPage) for page := 1; ; page++ { - curr, err := r.GetQueuePage(&starr.Req{PageSize: perPage, Page: page}) + curr, err := r.GetQueuePageContext(ctx, &starr.Req{PageSize: perPage, Page: page}) if err != nil { return nil, err } @@ -153,12 +80,18 @@ func (r *Radarr) GetQueue(records, perPage int) (*Queue, error) { //nolint:dupl // GetQueuePage returns a single page from the Radarr Queue. // The page size and number is configurable with the input request parameters. func (r *Radarr) GetQueuePage(params *starr.Req) (*Queue, error) { + return r.GetQueuePageContext(context.Background(), params) +} + +// GetQueuePage returns a single page from the Radarr Queue. +// The page size and number is configurable with the input request parameters. +func (r *Radarr) GetQueuePageContext(ctx context.Context, params *starr.Req) (*Queue, error) { var queue Queue params.CheckSet("sortKey", "timeleft") params.CheckSet("includeUnknownMovieItems", "true") - err := r.GetInto("v3/queue", params.Params(), &queue) + err := r.GetInto(ctx, "v3/queue", params.Params(), &queue) if err != nil { return nil, fmt.Errorf("api.Get(queue): %w", err) } @@ -166,362 +99,19 @@ func (r *Radarr) GetQueuePage(params *starr.Req) (*Queue, error) { return &queue, nil } -// GetMovie grabs a movie from the queue, or all movies if tmdbId is 0. -func (r *Radarr) GetMovie(tmdbID int64) ([]*Movie, error) { - params := make(url.Values) - if tmdbID != 0 { - params.Set("tmdbId", strconv.FormatInt(tmdbID, starr.Base10)) - } - - var movie []*Movie - - err := r.GetInto("v3/movie", params, &movie) - if err != nil { - return nil, fmt.Errorf("api.Get(movie): %w", err) - } - - return movie, nil -} - -// GetMovieByID grabs a movie from the database by DB [movie] ID. -func (r *Radarr) GetMovieByID(movieID int64) (*Movie, error) { - var movie Movie - - err := r.GetInto("v3/movie/"+strconv.FormatInt(movieID, starr.Base10), nil, &movie) - if err != nil { - return nil, fmt.Errorf("api.Get(movie): %w", err) - } - - return &movie, nil -} - -// GetQualityProfiles returns all configured quality profiles. -func (r *Radarr) GetQualityProfiles() ([]*QualityProfile, error) { - var profiles []*QualityProfile - - err := r.GetInto("v3/qualityProfile", nil, &profiles) - if err != nil { - return nil, fmt.Errorf("api.Get(qualityProfile): %w", err) - } - - return profiles, nil -} - -// AddQualityProfile updates a quality profile in place. -func (r *Radarr) AddQualityProfile(profile *QualityProfile) (int64, error) { - post, err := json.Marshal(profile) - if err != nil { - return 0, fmt.Errorf("json.Marshal(profile): %w", err) - } - - var output QualityProfile - - err = r.PostInto("v3/qualityProfile", nil, post, &output) - if err != nil { - return 0, fmt.Errorf("api.Post(qualityProfile): %w", err) - } - - return output.ID, nil -} - -// UpdateQualityProfile updates a quality profile in place. -func (r *Radarr) UpdateQualityProfile(profile *QualityProfile) error { - put, err := json.Marshal(profile) - if err != nil { - return fmt.Errorf("json.Marshal(profile): %w", err) - } - - _, err = r.Put("v3/qualityProfile/"+strconv.FormatInt(profile.ID, starr.Base10), nil, put) - if err != nil { - return fmt.Errorf("api.Put(qualityProfile): %w", err) - } - - return nil -} - // GetRootFolders returns all configured root folders. func (r *Radarr) GetRootFolders() ([]*RootFolder, error) { + return r.GetRootFoldersContext(context.Background()) +} + +// GetRootFoldersContext returns all configured root folders. +func (r *Radarr) GetRootFoldersContext(ctx context.Context) ([]*RootFolder, error) { var folders []*RootFolder - err := r.GetInto("v3/rootFolder", nil, &folders) + err := r.GetInto(ctx, "v3/rootFolder", nil, &folders) if err != nil { return nil, fmt.Errorf("api.Get(rootFolder): %w", err) } return folders, nil } - -// UpdateMovie sends a PUT request to update a movie in place. -func (r *Radarr) UpdateMovie(movieID int64, movie *Movie) error { - put, err := json.Marshal(movie) - if err != nil { - return fmt.Errorf("json.Marshal(movie): %w", err) - } - - params := make(url.Values) - params.Add("moveFiles", "true") - - _, err = r.Put("v3/movie/"+strconv.FormatInt(movieID, starr.Base10), params, put) - if err != nil { - return fmt.Errorf("api.Put(movie): %w", err) - } - - return nil -} - -// AddMovie adds a movie to the queue. -func (r *Radarr) AddMovie(movie *AddMovieInput) (*AddMovieOutput, error) { - body, err := json.Marshal(movie) - if err != nil { - return nil, fmt.Errorf("json.Marshal(movie): %w", err) - } - - params := make(url.Values) - params.Add("moveFiles", "true") - - var output AddMovieOutput - if err := r.PostInto("v3/movie", params, body, &output); err != nil { - return nil, fmt.Errorf("api.Post(movie): %w", err) - } - - return &output, nil -} - -// GetExclusions returns all configured exclusions from Radarr. -func (r *Radarr) GetExclusions() ([]*Exclusion, error) { - var exclusions []*Exclusion - - err := r.GetInto("v3/exclusions", nil, &exclusions) - if err != nil { - return nil, fmt.Errorf("api.Get(exclusions): %w", err) - } - - return exclusions, nil -} - -// DeleteExclusions removes exclusions from Radarr. -func (r *Radarr) DeleteExclusions(ids []int64) error { - var errs string - - for _, id := range ids { - _, err := r.Delete("v3/exclusions/"+strconv.FormatInt(id, starr.Base10), nil) - if err != nil { - errs += err.Error() + " " - } - } - - if errs != "" { - return fmt.Errorf("%w: %s", starr.ErrRequestError, errs) - } - - return nil -} - -// AddExclusions adds an exclusion to Radarr. -func (r *Radarr) AddExclusions(exclusions []*Exclusion) error { - for i := range exclusions { - exclusions[i].ID = 0 - } - - body, err := json.Marshal(exclusions) - if err != nil { - return fmt.Errorf("json.Marshal(movie): %w", err) - } - - _, err = r.Post("v3/exclusions/bulk", nil, body) - if err != nil { - return fmt.Errorf("api.Post(exclusions): %w", err) - } - - return nil -} - -// GetCustomFormats returns all configured Custom Formats. -func (r *Radarr) GetCustomFormats() ([]*CustomFormat, error) { - var output []*CustomFormat - if err := r.GetInto("v3/customFormat", nil, &output); err != nil { - return nil, fmt.Errorf("api.Get(customFormat): %w", err) - } - - return output, nil -} - -// AddCustomFormat creates a new custom format and returns the response (with ID). -func (r *Radarr) AddCustomFormat(format *CustomFormat) (*CustomFormat, error) { - var output CustomFormat - - if format == nil { - return &output, nil - } - - format.ID = 0 // ID must be zero when adding. - - body, err := json.Marshal(format) - if err != nil { - return nil, fmt.Errorf("json.Marshal(customFormat): %w", err) - } - - if err := r.PostInto("v3/customFormat", nil, body, &output); err != nil { - return nil, fmt.Errorf("api.Post(customFormat): %w", err) - } - - return &output, nil -} - -// UpdateCustomFormat updates an existing custom format and returns the response. -func (r *Radarr) UpdateCustomFormat(cf *CustomFormat, cfID int) (*CustomFormat, error) { - if cfID == 0 { - cfID = cf.ID - } - - body, err := json.Marshal(cf) - if err != nil { - return nil, fmt.Errorf("json.Marshal(customFormat): %w", err) - } - - var output CustomFormat - if err := r.PutInto("v3/customFormat/"+strconv.Itoa(cfID), nil, body, &output); err != nil { - return nil, fmt.Errorf("api.Put(customFormat): %w", err) - } - - return &output, nil -} - -// GetImportLists returns all import lists. -func (r *Radarr) GetImportLists() ([]*ImportList, error) { - var output []*ImportList - if err := r.GetInto("v3/importlist", nil, &output); err != nil { - return nil, fmt.Errorf("api.Get(importlist): %w", err) - } - - return output, nil -} - -// CreateImportList creates an import list in Radarr. -func (r *Radarr) CreateImportList(il *ImportList) (*ImportList, error) { - il.ID = 0 - - body, err := json.Marshal(il) - if err != nil { - return nil, fmt.Errorf("json.Marshal(importlist): %w", err) - } - - var output ImportList - if err := r.PostInto("v3/importlist", nil, body, &output); err != nil { - return nil, fmt.Errorf("api.Post(importlist): %w", err) - } - - return &output, nil -} - -// DeleteImportList removes an import list from Radarr. -func (r *Radarr) DeleteImportList(ids []int64) error { - var errs string - - for _, id := range ids { - _, err := r.Delete("v3/importlist/"+strconv.FormatInt(id, starr.Base10), nil) - if err != nil { - errs += fmt.Errorf("api.Delete(importlist): %w", err).Error() + " " - } - } - - if errs != "" { - return fmt.Errorf("%w: %s", starr.ErrRequestError, errs) - } - - return nil -} - -// UpdateImportList updates an existing import list and returns the response. -func (r *Radarr) UpdateImportList(list *ImportList) (*ImportList, error) { - body, err := json.Marshal(list) - if err != nil { - return nil, fmt.Errorf("json.Marshal(importlist): %w", err) - } - - var output ImportList - - err = r.PutInto("v3/importlist/"+strconv.FormatInt(list.ID, starr.Base10), nil, body, &output) - if err != nil { - return nil, fmt.Errorf("api.Put(importlist): %w", err) - } - - return &output, nil -} - -// GetCommands returns all available Radarr commands. -func (r *Radarr) GetCommands() ([]*CommandResponse, error) { - var output []*CommandResponse - - if err := r.GetInto("v3/command", nil, &output); err != nil { - return nil, fmt.Errorf("api.Get(command): %w", err) - } - - return output, nil -} - -// SendCommand sends a command to Radarr. -func (r *Radarr) SendCommand(cmd *CommandRequest) (*CommandResponse, error) { - var output CommandResponse - - if cmd == nil || cmd.Name == "" { - return &output, nil - } - - body, err := json.Marshal(cmd) - if err != nil { - return nil, fmt.Errorf("json.Marshal(cmd): %w", err) - } - - if err := r.PostInto("v3/command", nil, body, &output); err != nil { - return nil, fmt.Errorf("api.Post(command): %w", err) - } - - return &output, nil -} - -// Lookup will search for movies matching the specified search term. -func (r *Radarr) Lookup(term string) ([]*Movie, error) { - var output []*Movie - - if term == "" { - return output, nil - } - - params := make(url.Values) - params.Set("term", term) - - err := r.GetInto("v3/movie/lookup", params, &output) - if err != nil { - return nil, fmt.Errorf("api.Get(movie/lookup): %w", err) - } - - return output, nil -} - -// GetBackupFiles returns all available Radarr backup files. -// Use GetBody to download a file using BackupFile.Path. -func (r *Radarr) GetBackupFiles() ([]*starr.BackupFile, error) { - var output []*starr.BackupFile - - if err := r.GetInto("v3/system/backup", nil, &output); err != nil { - return nil, fmt.Errorf("api.Get(system/backup): %w", err) - } - - return output, nil -} - -// Fail marks the given history item as failed by id. -func (r *Radarr) Fail(historyID int64) error { - if historyID < 1 { - return fmt.Errorf("%w: invalid history ID: %d", starr.ErrRequestError, historyID) - } - - // Strangely uses a POST without a payload. - _, err := r.Post("v3/history/failed/"+strconv.FormatInt(historyID, starr.Base10), nil, nil) - if err != nil { - return fmt.Errorf("api.Post(history/failed): %w", err) - } - - return nil -} diff --git a/radarr/system.go b/radarr/system.go new file mode 100644 index 0000000..707dc2e --- /dev/null +++ b/radarr/system.go @@ -0,0 +1,43 @@ +package radarr + +import ( + "context" + "fmt" + + "golift.io/starr" +) + +// GetSystemStatus returns system status. +func (r *Radarr) GetSystemStatus() (*SystemStatus, error) { + return r.GetSystemStatusContext(context.Background()) +} + +// GetSystemStatusContext returns system status. +func (r *Radarr) GetSystemStatusContext(ctx context.Context) (*SystemStatus, error) { + var status SystemStatus + + err := r.GetInto(ctx, "v3/system/status", nil, &status) + if err != nil { + return nil, fmt.Errorf("api.Get(system/status): %w", err) + } + + return &status, nil +} + +// GetBackupFiles returns all available Radarr backup files. +// Use GetBody to download a file using BackupFile.Path. +func (r *Radarr) GetBackupFiles() ([]*starr.BackupFile, error) { + return r.GetBackupFilesContext(context.Background()) +} + +// GetBackupFilesContext returns all available Radarr backup files. +// Use GetBody to download a file using BackupFile.Path. +func (r *Radarr) GetBackupFilesContext(ctx context.Context) ([]*starr.BackupFile, error) { + var output []*starr.BackupFile + + if err := r.GetInto(ctx, "v3/system/backup", nil, &output); err != nil { + return nil, fmt.Errorf("api.Get(system/backup): %w", err) + } + + return output, nil +} diff --git a/radarr/tag.go b/radarr/tag.go new file mode 100644 index 0000000..c0c622c --- /dev/null +++ b/radarr/tag.go @@ -0,0 +1,66 @@ +package radarr + +import ( + "context" + "encoding/json" + "fmt" + + "golift.io/starr" +) + +// GetTags returns all the tags. +func (r *Radarr) GetTags() ([]*starr.Tag, error) { + return r.GetTagsContext(context.Background()) +} + +// GetTagsContext returns all the tags. +func (r *Radarr) GetTagsContext(ctx context.Context) ([]*starr.Tag, error) { + var tags []*starr.Tag + + err := r.GetInto(ctx, "v3/tag", nil, &tags) + if err != nil { + return nil, fmt.Errorf("api.Get(tag): %w", err) + } + + return tags, nil +} + +// UpdateTag updates the label for a tag. +func (r *Radarr) UpdateTag(tagID int, label string) (int, error) { + return r.UpdateTagContext(context.Background(), tagID, label) +} + +// UpdateTagContext updates the label for a tag. +func (r *Radarr) UpdateTagContext(ctx context.Context, tagID int, label string) (int, error) { + body, err := json.Marshal(&starr.Tag{Label: label, ID: tagID}) + if err != nil { + return 0, fmt.Errorf("json.Marshal(tag): %w", err) + } + + var tag starr.Tag + if err = r.PutInto(ctx, "v3/tag", nil, body, &tag); err != nil { + return tag.ID, fmt.Errorf("api.Put(tag): %w", err) + } + + return tag.ID, nil +} + +// AddTag adds a tag or returns the ID for an existing tag. +func (r *Radarr) AddTag(label string) (int, error) { + return r.AddTagContext(context.Background(), label) +} + +// AddTagContext adds a tag or returns the ID for an existing tag. +func (r *Radarr) AddTagContext(ctx context.Context, label string) (int, error) { + body, err := json.Marshal(&starr.Tag{Label: label, ID: 0}) + if err != nil { + return 0, fmt.Errorf("json.Marshal(tag): %w", err) + } + + var tag starr.Tag + if err = r.PostInto(ctx, "v3/tag", nil, body, &tag); err != nil { + return tag.ID, fmt.Errorf("api.Post(tag): %w", err) + } + + return tag.ID, nil +} diff --git a/radarr/type.go b/radarr/type.go index a97a62c..4b5faff 100644 --- a/radarr/type.go +++ b/radarr/type.go @@ -1,40 +1,11 @@ package radarr import ( - "crypto/tls" - "net/http" "time" "golift.io/starr" ) -// Radarr contains all the methods to interact with a Radarr server. -type Radarr struct { - starr.APIer -} - -// New returns a Radarr object used to interact with the Radarr API. -func New(config *starr.Config) *Radarr { - if config.Client == nil { - //nolint:exhaustivestruct,gosec - config.Client = &http.Client{ - Timeout: config.Timeout.Duration, - CheckRedirect: func(r *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - }, - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: !config.ValidSSL}, - }, - } - } - - if config.Debugf == nil { - config.Debugf = func(string, ...interface{}) {} - } - - return &Radarr{APIer: config} -} - // SystemStatus is the /api/v3/system/status endpoint. type SystemStatus struct { Version string `json:"version"` diff --git a/readarr/author.go b/readarr/author.go new file mode 100644 index 0000000..9e528b0 --- /dev/null +++ b/readarr/author.go @@ -0,0 +1,54 @@ +package readarr + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/url" + "strconv" + + "golift.io/starr" +) + +// GetAuthorByID returns an author. +func (r *Readarr) GetAuthorByID(authorID int64) (*Author, error) { + return r.GetAuthorByIDContext(context.Background(), authorID) +} + +// GetAuthorByIDContext returns an author. +func (r *Readarr) GetAuthorByIDContext(ctx context.Context, authorID int64) (*Author, error) { + var author Author + + err := r.GetInto(ctx, "v1/author/"+strconv.FormatInt(authorID, starr.Base10), nil, &author) + if err != nil { + return nil, fmt.Errorf("api.Get(author): %w", err) + } + + return &author, nil +} + +// UpdateAuthor updates an author in place. +func (r *Readarr) UpdateAuthor(authorID int64, author *Author) error { + return r.UpdateAuthorContext(context.Background(), authorID, author) +} + +// UpdateAuthorContext updates an author in place. +func (r *Readarr) UpdateAuthorContext(ctx context.Context, authorID int64, author *Author) error { + put, err := json.Marshal(author) + if err != nil { + return fmt.Errorf("json.Marshal(author): %w", err) + } + + params := make(url.Values) + params.Add("moveFiles", "true") + + b, err := r.Put(ctx, "v1/author/"+strconv.FormatInt(authorID, starr.Base10), params, put) + if err != nil { + return fmt.Errorf("api.Put(author): %w", err) + } + + log.Println("SHOW THIS TO CAPTAIN plz:", string(b)) + + return nil +} diff --git a/readarr/book.go b/readarr/book.go new file mode 100644 index 0000000..684d623 --- /dev/null +++ b/readarr/book.go @@ -0,0 +1,121 @@ +package readarr + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/url" + "strconv" + + "golift.io/starr" +) + +// GetBook returns books. All if gridID is empty. +func (r *Readarr) GetBook(gridID string) ([]*Book, error) { + return r.GetBookContext(context.Background(), gridID) +} + +func (r *Readarr) GetBookContext(ctx context.Context, gridID string) ([]*Book, error) { + params := make(url.Values) + + if gridID != "" { + params.Add("titleSlug", gridID) // this may change, but works for now. + } + + var books []*Book + + err := r.GetInto(ctx, "v1/book", params, &books) + if err != nil { + return nil, fmt.Errorf("api.Get(book): %w", err) + } + + return books, nil +} + +// GetBookByID returns a book. +func (r *Readarr) GetBookByID(bookID int64) (*Book, error) { + return r.GetBookByIDContext(context.Background(), bookID) +} + +func (r *Readarr) GetBookByIDContext(ctx context.Context, bookID int64) (*Book, error) { + var book Book + + err := r.GetInto(ctx, "v1/book/"+strconv.FormatInt(bookID, starr.Base10), nil, &book) + if err != nil { + return nil, fmt.Errorf("api.Get(book): %w", err) + } + + return &book, nil +} + +// UpdateBook updates a book in place. +func (r *Readarr) UpdateBook(bookID int64, book *Book) error { + return r.UpdateBookContext(context.Background(), bookID, book) +} + +func (r *Readarr) UpdateBookContext(ctx context.Context, bookID int64, book *Book) error { + put, err := json.Marshal(book) + if err != nil { + return fmt.Errorf("json.Marshal(book): %w", err) + } + + params := make(url.Values) + params.Add("moveFiles", "true") + + b, err := r.Put(ctx, "v1/book/"+strconv.FormatInt(bookID, starr.Base10), params, put) + if err != nil { + return fmt.Errorf("api.Put(book): %w", err) + } + + log.Println("SHOW THIS TO CAPTAIN plz:", string(b)) + + return nil +} + +// AddBook adds a new book to the library. +func (r *Readarr) AddBook(book *AddBookInput) (*AddBookOutput, error) { + return r.AddBookContext(context.Background(), book) +} + +func (r *Readarr) AddBookContext(ctx context.Context, book *AddBookInput) (*AddBookOutput, error) { + body, err := json.Marshal(book) + if err != nil { + return nil, fmt.Errorf("json.Marshal(book): %w", err) + } + + params := make(url.Values) + params.Add("moveFiles", "true") + + var output AddBookOutput + + err = r.PostInto(ctx, "v1/book", params, body, &output) + if err != nil { + return nil, fmt.Errorf("api.Post(book): %w", err) + } + + return &output, nil +} + +// Lookup will search for books matching the specified search term. +func (r *Readarr) Lookup(term string) ([]*Book, error) { + return r.LookupContext(context.Background(), term) +} + +func (r *Readarr) LookupContext(ctx context.Context, term string) ([]*Book, error) { + var output []*Book + + if term == "" { + return output, nil + } + + params := make(url.Values) + params.Set("term", term) + + err := r.GetInto(ctx, "v1/book/lookup", params, &output) + if err != nil { + return nil, fmt.Errorf("api.Get(book/lookup): %w", err) + } + + return output, nil +} diff --git a/readarr/command.go b/readarr/command.go new file mode 100644 index 0000000..743712e --- /dev/null +++ b/readarr/command.go @@ -0,0 +1,47 @@ +package readarr + +import ( + "context" + "encoding/json" + "fmt" +) + +// GetCommands returns all available Readarr commands. +// These can be used with SendCommand. +func (r *Readarr) GetCommands() ([]*CommandResponse, error) { + return r.GetCommandsContext(context.Background()) +} + +func (r *Readarr) GetCommandsContext(ctx context.Context) ([]*CommandResponse, error) { + var output []*CommandResponse + + if err := r.GetInto(ctx, "v1/command", nil, &output); err != nil { + return nil, fmt.Errorf("api.Get(command): %w", err) + } + + return output, nil +} + +// SendCommand sends a command to Readarr. +func (r *Readarr) SendCommand(cmd *CommandRequest) (*CommandResponse, error) { + return r.SendCommandContext(context.Background(), cmd) +} + +func (r *Readarr) SendCommandContext(ctx context.Context, cmd *CommandRequest) (*CommandResponse, error) { + var output CommandResponse + + if cmd == nil || cmd.Name == "" { + return &output, nil + } + + body, err := json.Marshal(cmd) + if err != nil { + return nil, fmt.Errorf("json.Marshal(cmd): %w", err) + } + + if err := r.PostInto(ctx, "v1/command", nil, body, &output); err != nil { + return nil, fmt.Errorf("api.Post(command): %w", err) + } + + return &output, nil +} diff --git a/readarr/history.go b/readarr/history.go new file mode 100644 index 0000000..316ec37 --- /dev/null +++ b/readarr/history.go @@ -0,0 +1,86 @@ +package readarr + +import ( + "context" + "fmt" + "strconv" + + "golift.io/starr" +) + +// GetHistory returns the Readarr History (grabs/failures/completed). +// WARNING: 12/30/2021 - this method changed. +// If you need control over the page, use readarr.GetHistoryPage(). +// This function simply returns the number of history records desired, +// up to the number of records present in the application. +// It grabs records in (paginated) batches of perPage, and concatenates +// them into one list. Passing zero for records will return all of them. +func (r *Readarr) GetHistory(records, perPage int) (*History, error) { + return r.GetHistoryContext(context.Background(), records, perPage) +} + +func (r *Readarr) GetHistoryContext(ctx context.Context, records, perPage int) (*History, error) { + hist := &History{Records: []HistoryRecord{}} + perPage = starr.SetPerPage(records, perPage) + + for page := 1; ; page++ { + curr, err := r.GetHistoryPageContext(ctx, &starr.Req{PageSize: perPage, Page: page}) + if err != nil { + return nil, err + } + + hist.Records = append(hist.Records, curr.Records...) + + if len(hist.Records) >= curr.TotalRecords || + (len(hist.Records) >= records && records != 0) || + len(curr.Records) == 0 { + hist.PageSize = curr.TotalRecords + hist.TotalRecords = curr.TotalRecords + hist.SortDirection = curr.SortDirection + hist.SortKey = curr.SortKey + + break + } + + perPage = starr.AdjustPerPage(records, curr.TotalRecords, len(hist.Records), perPage) + } + + return hist, nil +} + +// GetHistoryPage returns a single page from the Readarr History (grabs/failures/completed). +// The page size and number is configurable with the input request parameters. +func (r *Readarr) GetHistoryPage(params *starr.Req) (*History, error) { + return r.GetHistoryPageContext(context.Background(), params) +} + +func (r *Readarr) GetHistoryPageContext(ctx context.Context, params *starr.Req) (*History, error) { + var history History + + err := r.GetInto(ctx, "v1/history", params.Params(), &history) + if err != nil { + return nil, fmt.Errorf("api.Get(history): %w", err) + } + + return &history, nil +} + +// Fail marks the given history item as failed by id. +func (r *Readarr) Fail(historyID int64) error { + return r.FailContext(context.Background(), historyID) +} + +func (r *Readarr) FailContext(ctx context.Context, historyID int64) error { + if historyID < 1 { + return fmt.Errorf("%w: invalid history ID: %d", starr.ErrRequestError, historyID) + } + + post := []byte("id=" + strconv.FormatInt(historyID, starr.Base10)) + + _, err := r.Post(ctx, "v1/history/failed", nil, post) + if err != nil { + return fmt.Errorf("api.Post(history/failed): %w", err) + } + + return nil +} diff --git a/readarr/qualityprofile.go b/readarr/qualityprofile.go new file mode 100644 index 0000000..164fc7c --- /dev/null +++ b/readarr/qualityprofile.go @@ -0,0 +1,66 @@ +package readarr + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + + "golift.io/starr" +) + +// GetQualityProfiles returns the quality profiles. +func (r *Readarr) GetQualityProfiles() ([]*QualityProfile, error) { + return r.GetQualityProfilesContext(context.Background()) +} + +func (r *Readarr) GetQualityProfilesContext(ctx context.Context) ([]*QualityProfile, error) { + var profiles []*QualityProfile + + err := r.GetInto(ctx, "v1/qualityprofile", nil, &profiles) + if err != nil { + return nil, fmt.Errorf("api.Get(qualityprofile): %w", err) + } + + return profiles, nil +} + +// AddQualityProfile updates a quality profile in place. +func (r *Readarr) AddQualityProfile(profile *QualityProfile) (int64, error) { + return r.AddQualityProfileContext(context.Background(), profile) +} + +func (r *Readarr) AddQualityProfileContext(ctx context.Context, profile *QualityProfile) (int64, error) { + post, err := json.Marshal(profile) + if err != nil { + return 0, fmt.Errorf("json.Marshal(profile): %w", err) + } + + var output QualityProfile + + err = r.PostInto(ctx, "v1/qualityProfile", nil, post, &output) + if err != nil { + return 0, fmt.Errorf("api.Post(qualityProfile): %w", err) + } + + return output.ID, nil +} + +// UpdateQualityProfile updates a quality profile in place. +func (r *Readarr) UpdateQualityProfile(profile *QualityProfile) error { + return r.UpdateQualityProfileContext(context.Background(), profile) +} + +func (r *Readarr) UpdateQualityProfileContext(ctx context.Context, profile *QualityProfile) error { + put, err := json.Marshal(profile) + if err != nil { + return fmt.Errorf("json.Marshal(profile): %w", err) + } + + _, err = r.Put(ctx, "v1/qualityProfile/"+strconv.FormatInt(profile.ID, starr.Base10), nil, put) + if err != nil { + return fmt.Errorf("api.Put(qualityProfile): %w", err) + } + + return nil +} diff --git a/readarr/readarr.go b/readarr/readarr.go index 74d0c60..ebb3040 100644 --- a/readarr/readarr.go +++ b/readarr/readarr.go @@ -1,55 +1,39 @@ package readarr import ( - "encoding/json" + "context" + "crypto/tls" "fmt" - "log" - "net/url" - "strconv" + "net/http" "golift.io/starr" ) -// GetTags returns all the tags. -func (r *Readarr) GetTags() ([]*starr.Tag, error) { - var tags []*starr.Tag - - err := r.GetInto("v1/tag", nil, &tags) - if err != nil { - return nil, fmt.Errorf("api.Get(tag): %w", err) - } - - return tags, nil -} - -// UpdateTag updates the label for a tag. -func (r *Readarr) UpdateTag(tagID int, label string) (int, error) { - body, err := json.Marshal(&starr.Tag{Label: label, ID: tagID}) - if err != nil { - return 0, fmt.Errorf("json.Marshal(tag): %w", err) - } - - var tag starr.Tag - if err = r.PutInto("v1/tag", nil, body, &tag); err != nil { - return tag.ID, fmt.Errorf("api.Put(tag): %w", err) - } - - return tag.ID, nil -} - -// AddTag adds a tag or returns the ID for an existing tag. -func (r *Readarr) AddTag(label string) (int, error) { - body, err := json.Marshal(&starr.Tag{Label: label, ID: 0}) - if err != nil { - return 0, fmt.Errorf("json.Marshal(tag): %w", err) +// Readarr contains all the methods to interact with a Readarr server. +type Readarr struct { + starr.APIer +} + +// New returns a Readarr object used to interact with the Readarr API. +func New(config *starr.Config) *Readarr { + if config.Client == nil { + //nolint:exhaustivestruct,gosec + config.Client = &http.Client{ + Timeout: config.Timeout.Duration, + CheckRedirect: func(r *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: !config.ValidSSL}, + }, + } } - var tag starr.Tag - if err = r.PostInto("v1/tag", nil, body, &tag); err != nil { - return tag.ID, fmt.Errorf("api.Post(tag): %w", err) + if config.Debugf == nil { + config.Debugf = func(string, ...interface{}) {} } - return tag.ID, nil + return &Readarr{APIer: config} } // GetQueue returns a single page from the Readarr Queue (processing, but not yet imported). @@ -60,11 +44,15 @@ func (r *Readarr) AddTag(label string) (int, error) { // It grabs records in (paginated) batches of perPage, and concatenates // them into one list. Passing zero for records will return all of them. func (r *Readarr) GetQueue(records, perPage int) (*Queue, error) { + return r.GetQueueContext(context.Background(), records, perPage) +} + +func (r *Readarr) GetQueueContext(ctx context.Context, records, perPage int) (*Queue, error) { queue := &Queue{Records: []*QueueRecord{}} perPage = starr.SetPerPage(records, perPage) for page := 1; ; page++ { - curr, err := r.GetQueuePage(&starr.Req{PageSize: perPage, Page: page}) + curr, err := r.GetQueuePageContext(ctx, &starr.Req{PageSize: perPage, Page: page}) if err != nil { return nil, err } @@ -91,12 +79,16 @@ func (r *Readarr) GetQueue(records, perPage int) (*Queue, error) { // GetQueuePage returns a single page from the Readarr Queue. // The page size and number is configurable with the input request parameters. func (r *Readarr) GetQueuePage(params *starr.Req) (*Queue, error) { + return r.GetQueuePageContext(context.Background(), params) +} + +func (r *Readarr) GetQueuePageContext(ctx context.Context, params *starr.Req) (*Queue, error) { var queue Queue params.CheckSet("sortKey", "timeleft") params.CheckSet("includeUnknownAuthorItems", "true") - err := r.GetInto("v1/queue", params.Params(), &queue) + err := r.GetInto(ctx, "v1/queue", params.Params(), &queue) if err != nil { return nil, fmt.Errorf("api.Get(queue): %w", err) } @@ -104,23 +96,15 @@ func (r *Readarr) GetQueuePage(params *starr.Req) (*Queue, error) { return &queue, nil } -// GetSystemStatus returns system status. -func (r *Readarr) GetSystemStatus() (*SystemStatus, error) { - var status SystemStatus - - err := r.GetInto("v1/system/status", nil, &status) - if err != nil { - return &status, fmt.Errorf("api.Get(system/status): %w", err) - } - - return &status, nil -} - // GetRootFolders returns all configured root folders. func (r *Readarr) GetRootFolders() ([]*RootFolder, error) { + return r.GetRootFoldersContext(context.Background()) +} + +func (r *Readarr) GetRootFoldersContext(ctx context.Context) ([]*RootFolder, error) { var folders []*RootFolder - err := r.GetInto("v1/rootFolder", nil, &folders) + err := r.GetInto(ctx, "v1/rootFolder", nil, &folders) if err != nil { return nil, fmt.Errorf("api.Get(rootFolder): %w", err) } @@ -130,286 +114,16 @@ func (r *Readarr) GetRootFolders() ([]*RootFolder, error) { // GetMetadataProfiles returns the metadata profiles. func (r *Readarr) GetMetadataProfiles() ([]*MetadataProfile, error) { - var profiles []*MetadataProfile - - err := r.GetInto("v1/metadataprofile", nil, &profiles) - if err != nil { - return nil, fmt.Errorf("api.Get(metadataprofile): %w", err) - } - - return profiles, nil + return r.GetMetadataProfilesContext(context.Background()) } -// GetQualityProfiles returns the quality profiles. -func (r *Readarr) GetQualityProfiles() ([]*QualityProfile, error) { - var profiles []*QualityProfile +func (r *Readarr) GetMetadataProfilesContext(ctx context.Context) ([]*MetadataProfile, error) { + var profiles []*MetadataProfile - err := r.GetInto("v1/qualityprofile", nil, &profiles) + err := r.GetInto(ctx, "v1/metadataprofile", nil, &profiles) if err != nil { - return nil, fmt.Errorf("api.Get(qualityprofile): %w", err) + return nil, fmt.Errorf("api.Get(metadataprofile): %w", err) } return profiles, nil } - -// AddQualityProfile updates a quality profile in place. -func (r *Readarr) AddQualityProfile(profile *QualityProfile) (int64, error) { - post, err := json.Marshal(profile) - if err != nil { - return 0, fmt.Errorf("json.Marshal(profile): %w", err) - } - - var output QualityProfile - - err = r.PostInto("v1/qualityProfile", nil, post, &output) - if err != nil { - return 0, fmt.Errorf("api.Post(qualityProfile): %w", err) - } - - return output.ID, nil -} - -// UpdateQualityProfile updates a quality profile in place. -func (r *Readarr) UpdateQualityProfile(profile *QualityProfile) error { - put, err := json.Marshal(profile) - if err != nil { - return fmt.Errorf("json.Marshal(profile): %w", err) - } - - _, err = r.Put("v1/qualityProfile/"+strconv.FormatInt(profile.ID, starr.Base10), nil, put) - if err != nil { - return fmt.Errorf("api.Put(qualityProfile): %w", err) - } - - return nil -} - -// GetAuthorByID returns an author. -func (r *Readarr) GetAuthorByID(authorID int64) (*Author, error) { - var author Author - - err := r.GetInto("v1/author/"+strconv.FormatInt(authorID, starr.Base10), nil, &author) - if err != nil { - return nil, fmt.Errorf("api.Get(author): %w", err) - } - - return &author, nil -} - -// UpdateAuthor updates an author in place. -func (r *Readarr) UpdateAuthor(authorID int64, author *Author) error { - put, err := json.Marshal(author) - if err != nil { - return fmt.Errorf("json.Marshal(author): %w", err) - } - - params := make(url.Values) - params.Add("moveFiles", "true") - - b, err := r.Put("v1/author/"+strconv.FormatInt(authorID, starr.Base10), params, put) - if err != nil { - return fmt.Errorf("api.Put(author): %w", err) - } - - log.Println("SHOW THIS TO CAPTAIN plz:", string(b)) - - return nil -} - -// GetBook returns books. All if gridID is empty. -func (r *Readarr) GetBook(gridID string) ([]*Book, error) { - params := make(url.Values) - - if gridID != "" { - params.Add("titleSlug", gridID) // this may change, but works for now. - } - - var books []*Book - - err := r.GetInto("v1/book", params, &books) - if err != nil { - return nil, fmt.Errorf("api.Get(book): %w", err) - } - - return books, nil -} - -// GetBookByID returns a book. -func (r *Readarr) GetBookByID(bookID int64) (*Book, error) { - var book Book - - err := r.GetInto("v1/book/"+strconv.FormatInt(bookID, starr.Base10), nil, &book) - if err != nil { - return nil, fmt.Errorf("api.Get(book): %w", err) - } - - return &book, nil -} - -// UpdateBook updates a book in place. -func (r *Readarr) UpdateBook(bookID int64, book *Book) error { - put, err := json.Marshal(book) - if err != nil { - return fmt.Errorf("json.Marshal(book): %w", err) - } - - params := make(url.Values) - params.Add("moveFiles", "true") - - b, err := r.Put("v1/book/"+strconv.FormatInt(bookID, starr.Base10), params, put) - if err != nil { - return fmt.Errorf("api.Put(book): %w", err) - } - - log.Println("SHOW THIS TO CAPTAIN plz:", string(b)) - - return nil -} - -// AddBook adds a new book to the library. -func (r *Readarr) AddBook(book *AddBookInput) (*AddBookOutput, error) { - body, err := json.Marshal(book) - if err != nil { - return nil, fmt.Errorf("json.Marshal(book): %w", err) - } - - params := make(url.Values) - params.Add("moveFiles", "true") - - var output AddBookOutput - - err = r.PostInto("v1/book", params, body, &output) - if err != nil { - return nil, fmt.Errorf("api.Post(book): %w", err) - } - - return &output, nil -} - -// GetCommands returns all available Readarr commands. -// These can be used with SendCommand. -func (r *Readarr) GetCommands() ([]*CommandResponse, error) { - var output []*CommandResponse - - if err := r.GetInto("v1/command", nil, &output); err != nil { - return nil, fmt.Errorf("api.Get(command): %w", err) - } - - return output, nil -} - -// SendCommand sends a command to Readarr. -func (r *Readarr) SendCommand(cmd *CommandRequest) (*CommandResponse, error) { - var output CommandResponse - - if cmd == nil || cmd.Name == "" { - return &output, nil - } - - body, err := json.Marshal(cmd) - if err != nil { - return nil, fmt.Errorf("json.Marshal(cmd): %w", err) - } - - if err := r.PostInto("v1/command", nil, body, &output); err != nil { - return nil, fmt.Errorf("api.Post(command): %w", err) - } - - return &output, nil -} - -// GetHistory returns the Readarr History (grabs/failures/completed). -// WARNING: 12/30/2021 - this method changed. -// If you need control over the page, use readarr.GetHistoryPage(). -// This function simply returns the number of history records desired, -// up to the number of records present in the application. -// It grabs records in (paginated) batches of perPage, and concatenates -// them into one list. Passing zero for records will return all of them. -func (r *Readarr) GetHistory(records, perPage int) (*History, error) { - hist := &History{Records: []HistoryRecord{}} - perPage = starr.SetPerPage(records, perPage) - - for page := 1; ; page++ { - curr, err := r.GetHistoryPage(&starr.Req{PageSize: perPage, Page: page}) - if err != nil { - return nil, err - } - - hist.Records = append(hist.Records, curr.Records...) - - if len(hist.Records) >= curr.TotalRecords || - (len(hist.Records) >= records && records != 0) || - len(curr.Records) == 0 { - hist.PageSize = curr.TotalRecords - hist.TotalRecords = curr.TotalRecords - hist.SortDirection = curr.SortDirection - hist.SortKey = curr.SortKey - - break - } - - perPage = starr.AdjustPerPage(records, curr.TotalRecords, len(hist.Records), perPage) - } - - return hist, nil -} - -// GetHistoryPage returns a single page from the Readarr History (grabs/failures/completed). -// The page size and number is configurable with the input request parameters. -func (r *Readarr) GetHistoryPage(params *starr.Req) (*History, error) { - var history History - - err := r.GetInto("v1/history", params.Params(), &history) - if err != nil { - return nil, fmt.Errorf("api.Get(history): %w", err) - } - - return &history, nil -} - -// Fail marks the given history item as failed by id. -func (r *Readarr) Fail(historyID int64) error { - if historyID < 1 { - return fmt.Errorf("%w: invalid history ID: %d", starr.ErrRequestError, historyID) - } - - post := []byte("id=" + strconv.FormatInt(historyID, starr.Base10)) - - _, err := r.Post("v1/history/failed", nil, post) - if err != nil { - return fmt.Errorf("api.Post(history/failed): %w", err) - } - - return nil -} - -// Lookup will search for books matching the specified search term. -func (r *Readarr) Lookup(term string) ([]*Book, error) { - var output []*Book - - if term == "" { - return output, nil - } - - params := make(url.Values) - params.Set("term", term) - - err := r.GetInto("v1/book/lookup", params, &output) - if err != nil { - return nil, fmt.Errorf("api.Get(book/lookup): %w", err) - } - - return output, nil -} - -// GetBackupFiles returns all available Readarr backup files. -// Use GetBody to download a file using BackupFile.Path. -func (r *Readarr) GetBackupFiles() ([]*starr.BackupFile, error) { - var output []*starr.BackupFile - - if err := r.GetInto("v1/system/backup", nil, &output); err != nil { - return nil, fmt.Errorf("api.Get(system/backup): %w", err) - } - - return output, nil -} diff --git a/readarr/system.go b/readarr/system.go new file mode 100644 index 0000000..e99a1ae --- /dev/null +++ b/readarr/system.go @@ -0,0 +1,40 @@ +package readarr + +import ( + "context" + "fmt" + + "golift.io/starr" +) + +// GetSystemStatus returns system status. +func (r *Readarr) GetSystemStatus() (*SystemStatus, error) { + return r.GetSystemStatusContext(context.Background()) +} + +func (r *Readarr) GetSystemStatusContext(ctx context.Context) (*SystemStatus, error) { + var status SystemStatus + + err := r.GetInto(ctx, "v1/system/status", nil, &status) + if err != nil { + return &status, fmt.Errorf("api.Get(system/status): %w", err) + } + + return &status, nil +} + +// GetBackupFiles returns all available Readarr backup files. +// Use GetBody to download a file using BackupFile.Path. +func (r *Readarr) GetBackupFiles() ([]*starr.BackupFile, error) { + return r.GetBackupFilesContext(context.Background()) +} + +func (r *Readarr) GetBackupFilesContext(ctx context.Context) ([]*starr.BackupFile, error) { + var output []*starr.BackupFile + + if err := r.GetInto(ctx, "v1/system/backup", nil, &output); err != nil { + return nil, fmt.Errorf("api.Get(system/backup): %w", err) + } + + return output, nil +} diff --git a/readarr/tag.go b/readarr/tag.go new file mode 100644 index 0000000..6153fdb --- /dev/null +++ b/readarr/tag.go @@ -0,0 +1,63 @@ +package readarr + +import ( + "context" + "encoding/json" + "fmt" + + "golift.io/starr" +) + +// GetTags returns all the tags. +func (r *Readarr) GetTags() ([]*starr.Tag, error) { + return r.GetTagsContext(context.Background()) +} + +func (r *Readarr) GetTagsContext(ctx context.Context) ([]*starr.Tag, error) { + var tags []*starr.Tag + + err := r.GetInto(ctx, "v1/tag", nil, &tags) + if err != nil { + return nil, fmt.Errorf("api.Get(tag): %w", err) + } + + return tags, nil +} + +// UpdateTag updates the label for a tag. +func (r *Readarr) UpdateTag(tagID int, label string) (int, error) { + return r.UpdateTagContext(context.Background(), tagID, label) +} + +func (r *Readarr) UpdateTagContext(ctx context.Context, tagID int, label string) (int, error) { + body, err := json.Marshal(&starr.Tag{Label: label, ID: tagID}) + if err != nil { + return 0, fmt.Errorf("json.Marshal(tag): %w", err) + } + + var tag starr.Tag + if err = r.PutInto(ctx, "v1/tag", nil, body, &tag); err != nil { + return tag.ID, fmt.Errorf("api.Put(tag): %w", err) + } + + return tag.ID, nil +} + +// AddTag adds a tag or returns the ID for an existing tag. +func (r *Readarr) AddTag(label string) (int, error) { + return r.AddTagContext(context.Background(), label) +} + +func (r *Readarr) AddTagContext(ctx context.Context, label string) (int, error) { + body, err := json.Marshal(&starr.Tag{Label: label, ID: 0}) + if err != nil { + return 0, fmt.Errorf("json.Marshal(tag): %w", err) + } + + var tag starr.Tag + if err = r.PostInto(ctx, "v1/tag", nil, body, &tag); err != nil { + return tag.ID, fmt.Errorf("api.Post(tag): %w", err) + } + + return tag.ID, nil +} diff --git a/readarr/type.go b/readarr/type.go index 4049a1a..5416112 100644 --- a/readarr/type.go +++ b/readarr/type.go @@ -1,40 +1,11 @@ package readarr import ( - "crypto/tls" - "net/http" "time" "golift.io/starr" ) -// Readarr contains all the methods to interact with a Readarr server. -type Readarr struct { - starr.APIer -} - -// New returns a Readarr object used to interact with the Readarr API. -func New(config *starr.Config) *Readarr { - if config.Client == nil { - //nolint:exhaustivestruct,gosec - config.Client = &http.Client{ - Timeout: config.Timeout.Duration, - CheckRedirect: func(r *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - }, - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: !config.ValidSSL}, - }, - } - } - - if config.Debugf == nil { - config.Debugf = func(string, ...interface{}) {} - } - - return &Readarr{APIer: config} -} - // Queue is the /api/v1/queue endpoint. type Queue struct { Page int `json:"page"` diff --git a/sonarr/command.go b/sonarr/command.go new file mode 100644 index 0000000..d8de2f0 --- /dev/null +++ b/sonarr/command.go @@ -0,0 +1,70 @@ +package sonarr + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + + "golift.io/starr" +) + +// GetCommands returns all available Sonarr commands. +// These can be used with SendCommand. +func (s *Sonarr) GetCommands() ([]*CommandResponse, error) { + return s.GetCommandsContext(context.Background()) +} + +func (s *Sonarr) GetCommandsContext(ctx context.Context) ([]*CommandResponse, error) { + var output []*CommandResponse + + if err := s.GetInto(ctx, "v3/command", nil, &output); err != nil { + return nil, fmt.Errorf("api.Get(command): %w", err) + } + + return output, nil +} + +// SendCommand sends a command to Sonarr. +func (s *Sonarr) SendCommand(cmd *CommandRequest) (*CommandResponse, error) { + return s.SendCommandContext(context.Background(), cmd) +} + +func (s *Sonarr) SendCommandContext(ctx context.Context, cmd *CommandRequest) (*CommandResponse, error) { + var output CommandResponse + + if cmd == nil || cmd.Name == "" { + return &output, nil + } + + body, err := json.Marshal(cmd) + if err != nil { + return nil, fmt.Errorf("json.Marshal(cmd): %w", err) + } + + if err := s.PostInto(ctx, "v3/command", nil, body, &output); err != nil { + return nil, fmt.Errorf("api.Post(command): %w", err) + } + + return &output, nil +} + +// GetCommandStatus returns the status of an already started command. +func (s *Sonarr) GetCommandStatus(commandID int64) (*CommandResponse, error) { + return s.GetCommandStatusContext(context.Background(), commandID) +} + +func (s *Sonarr) GetCommandStatusContext(ctx context.Context, commandID int64) (*CommandResponse, error) { + var output CommandResponse + + if commandID == 0 { + return &output, nil + } + + err := s.GetInto(ctx, "v3/command/"+strconv.FormatInt(commandID, starr.Base10), nil, &output) + if err != nil { + return nil, fmt.Errorf("api.Post(command): %w", err) + } + + return &output, nil +} diff --git a/sonarr/episode.go b/sonarr/episode.go new file mode 100644 index 0000000..c968be5 --- /dev/null +++ b/sonarr/episode.go @@ -0,0 +1,53 @@ +package sonarr + +import ( + "context" + "encoding/json" + "fmt" + "net/url" + "strconv" + + "golift.io/starr" +) + +// GetSeriesEpisodes returns all episodes for a series by series ID. +// You can get series IDs from GetAllSeries() and GetSeries(). +func (s *Sonarr) GetSeriesEpisodes(seriesID int64) ([]*Episode, error) { + return s.GetSeriesEpisodesContext(context.Background(), seriesID) +} + +func (s *Sonarr) GetSeriesEpisodesContext(ctx context.Context, seriesID int64) ([]*Episode, error) { + var output []*Episode + + params := make(url.Values) + params.Add("seriesId", strconv.FormatInt(seriesID, starr.Base10)) + + err := s.GetInto(ctx, "v3/episode?seriesId", params, &output) + if err != nil { + return nil, fmt.Errorf("api.Get(episode): %w", err) + } + + return output, nil +} + +// MonitorEpisode sends a request to monitor (true) or unmonitor (false) a list of episodes by ID. +// You can get episode IDs from GetSeriesEpisodes(). +func (s *Sonarr) MonitorEpisode(episodeIDs []int64, monitor bool) ([]*Episode, error) { + return s.MonitorEpisodeContext(context.Background(), episodeIDs, monitor) +} + +func (s *Sonarr) MonitorEpisodeContext(ctx context.Context, episodeIDs []int64, monitor bool) ([]*Episode, error) { + var ( + input, _ = json.Marshal(&struct { + E []int64 `json:"episodeIds"` + M bool `json:"monitored"` + }{E: episodeIDs, M: monitor}) + output []*Episode + ) + + if err := s.PutInto(ctx, "v3/episode/monitor", nil, input, &output); err != nil { + return nil, fmt.Errorf("api.Put(episode/monitor): %w", err) + } + + return output, nil +} diff --git a/sonarr/history.go b/sonarr/history.go new file mode 100644 index 0000000..22682c4 --- /dev/null +++ b/sonarr/history.go @@ -0,0 +1,85 @@ +package sonarr + +import ( + "context" + "fmt" + "strconv" + + "golift.io/starr" +) + +// GetHistory returns the Sonarr History (grabs/failures/completed). +// WARNING: 12/30/2021 - this method changed. +// If you need control over the page, use sonarr.GetHistoryPage(). +// This function simply returns the number of history records desired, +// up to the number of records present in the application. +// It grabs records in (paginated) batches of perPage, and concatenates +// them into one list. Passing zero for records will return all of them. +func (s *Sonarr) GetHistory(records, perPage int) (*History, error) { + return s.GetHistoryContext(context.Background(), records, perPage) +} + +func (s *Sonarr) GetHistoryContext(ctx context.Context, records, perPage int) (*History, error) { + hist := &History{Records: []*HistoryRecord{}} + perPage = starr.SetPerPage(records, perPage) + + for page := 1; ; page++ { + curr, err := s.GetHistoryPageContext(ctx, &starr.Req{PageSize: perPage, Page: page}) + if err != nil { + return nil, err + } + + hist.Records = append(hist.Records, curr.Records...) + + if len(hist.Records) >= curr.TotalRecords || + (len(hist.Records) >= records && records != 0) || + len(curr.Records) == 0 { + hist.PageSize = curr.TotalRecords + hist.TotalRecords = curr.TotalRecords + hist.SortDirection = curr.SortDirection + hist.SortKey = curr.SortKey + + break + } + + perPage = starr.AdjustPerPage(records, curr.TotalRecords, len(hist.Records), perPage) + } + + return hist, nil +} + +// GetHistoryPage returns a single page from the Sonarr History (grabs/failures/completed). +// The page size and number is configurable with the input request parameters. +func (s *Sonarr) GetHistoryPage(params *starr.Req) (*History, error) { + return s.GetHistoryPageContext(context.Background(), params) +} + +func (s *Sonarr) GetHistoryPageContext(ctx context.Context, params *starr.Req) (*History, error) { + var history History + + err := s.GetInto(ctx, "v3/history", params.Params(), &history) + if err != nil { + return nil, fmt.Errorf("api.Get(history): %w", err) + } + + return &history, nil +} + +// Fail marks the given history item as failed by id. +func (s *Sonarr) Fail(historyID int64) error { + return s.FailContext(context.Background(), historyID) +} + +func (s *Sonarr) FailContext(ctx context.Context, historyID int64) error { + if historyID < 1 { + return fmt.Errorf("%w: invalid history ID: %d", starr.ErrRequestError, historyID) + } + + // Strangely uses a POST without a payload. + _, err := s.Post(ctx, "v3/history/failed/"+strconv.FormatInt(historyID, starr.Base10), nil, nil) + if err != nil { + return fmt.Errorf("api.Post(history/failed): %w", err) + } + + return nil +} diff --git a/sonarr/qualityprofile.go b/sonarr/qualityprofile.go new file mode 100644 index 0000000..82f6b49 --- /dev/null +++ b/sonarr/qualityprofile.go @@ -0,0 +1,66 @@ +package sonarr + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + + "golift.io/starr" +) + +// GetQualityProfiles returns all configured quality profiles. +func (s *Sonarr) GetQualityProfiles() ([]*QualityProfile, error) { + return s.GetQualityProfilesContext(context.Background()) +} + +func (s *Sonarr) GetQualityProfilesContext(ctx context.Context) ([]*QualityProfile, error) { + var profiles []*QualityProfile + + err := s.GetInto(ctx, "v3/qualityprofile", nil, &profiles) + if err != nil { + return nil, fmt.Errorf("api.Get(qualityprofile): %w", err) + } + + return profiles, nil +} + +// AddQualityProfile updates a quality profile in place. +func (s *Sonarr) AddQualityProfile(profile *QualityProfile) (int64, error) { + return s.AddQualityProfileContext(context.Background(), profile) +} + +func (s *Sonarr) AddQualityProfileContext(ctx context.Context, profile *QualityProfile) (int64, error) { + post, err := json.Marshal(profile) + if err != nil { + return 0, fmt.Errorf("json.Marshal(profile): %w", err) + } + + var output QualityProfile + + err = s.PostInto(ctx, "v3/qualityProfile", nil, post, &output) + if err != nil { + return 0, fmt.Errorf("api.Post(qualityProfile): %w", err) + } + + return output.ID, nil +} + +// UpdateQualityProfile updates a quality profile in place. +func (s *Sonarr) UpdateQualityProfile(profile *QualityProfile) error { + return s.UpdateQualityProfileContext(context.Background(), profile) +} + +func (s *Sonarr) UpdateQualityProfileContext(ctx context.Context, profile *QualityProfile) error { + put, err := json.Marshal(profile) + if err != nil { + return fmt.Errorf("json.Marshal(profile): %w", err) + } + + _, err = s.Put(ctx, "v3/qualityProfile/"+strconv.FormatInt(profile.ID, starr.Base10), nil, put) + if err != nil { + return fmt.Errorf("api.Put(qualityProfile): %w", err) + } + + return nil +} diff --git a/sonarr/releaseprofile.go b/sonarr/releaseprofile.go new file mode 100644 index 0000000..7c024ca --- /dev/null +++ b/sonarr/releaseprofile.go @@ -0,0 +1,66 @@ +package sonarr + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + + "golift.io/starr" +) + +// GetReleaseProfiles returns all configured release profiles. +func (s *Sonarr) GetReleaseProfiles() ([]*ReleaseProfile, error) { + return s.GetReleaseProfilesContext(context.Background()) +} + +func (s *Sonarr) GetReleaseProfilesContext(ctx context.Context) ([]*ReleaseProfile, error) { + var profiles []*ReleaseProfile + + err := s.GetInto(ctx, "v3/releaseProfile", nil, &profiles) + if err != nil { + return nil, fmt.Errorf("api.Get(releaseProfile): %w", err) + } + + return profiles, nil +} + +// AddReleaseProfile updates a release profile in place. +func (s *Sonarr) AddReleaseProfile(profile *ReleaseProfile) (int64, error) { + return s.AddReleaseProfileContext(context.Background(), profile) +} + +func (s *Sonarr) AddReleaseProfileContext(ctx context.Context, profile *ReleaseProfile) (int64, error) { + post, err := json.Marshal(profile) + if err != nil { + return 0, fmt.Errorf("json.Marshal(profile): %w", err) + } + + var output ReleaseProfile + + err = s.PostInto(ctx, "v3/releaseProfile", nil, post, &output) + if err != nil { + return 0, fmt.Errorf("api.Post(releaseProfile): %w", err) + } + + return output.ID, nil +} + +// UpdateReleaseProfile updates a release profile in place. +func (s *Sonarr) UpdateReleaseProfile(profile *ReleaseProfile) error { + return s.UpdateReleaseProfileContext(context.Background(), profile) +} + +func (s *Sonarr) UpdateReleaseProfileContext(ctx context.Context, profile *ReleaseProfile) error { + put, err := json.Marshal(profile) + if err != nil { + return fmt.Errorf("json.Marshal(profile): %w", err) + } + + _, err = s.Put(ctx, "v3/releaseProfile/"+strconv.FormatInt(profile.ID, starr.Base10), nil, put) + if err != nil { + return fmt.Errorf("api.Put(releaseProfile): %w", err) + } + + return nil +} diff --git a/sonarr/series.go b/sonarr/series.go new file mode 100644 index 0000000..5c34099 --- /dev/null +++ b/sonarr/series.go @@ -0,0 +1,141 @@ +package sonarr + +import ( + "context" + "encoding/json" + "fmt" + "log" + "net/url" + "strconv" + + "golift.io/starr" +) + +// GetAllSeries returns all configured series. +// This may not deal well with pagination atm. +func (s *Sonarr) GetAllSeries() ([]*Series, error) { + return s.GetAllSeriesContext(context.Background()) +} + +func (s *Sonarr) GetAllSeriesContext(ctx context.Context) ([]*Series, error) { + return s.GetSeriesContext(ctx, 0) +} + +// GetSeries locates and returns a series by tvdbID. If tvdbID is 0, returns all series. +func (s *Sonarr) GetSeries(tvdbID int64) ([]*Series, error) { + return s.GetSeriesContext(context.Background(), tvdbID) +} + +func (s *Sonarr) GetSeriesContext(ctx context.Context, tvdbID int64) ([]*Series, error) { + params := make(url.Values) + + if tvdbID != 0 { + params.Add("tvdbId", strconv.FormatInt(tvdbID, starr.Base10)) + } + + var series []*Series + + err := s.GetInto(ctx, "v3/series", params, &series) + if err != nil { + return nil, fmt.Errorf("api.Get(series): %w", err) + } + + return series, nil +} + +// UpdateSeries updates a series in place. +func (s *Sonarr) UpdateSeries(seriesID int64, series *Series) error { + return s.UpdateSeriesContext(context.Background(), seriesID, series) +} + +func (s *Sonarr) UpdateSeriesContext(ctx context.Context, seriesID int64, series *Series) error { + put, err := json.Marshal(series) + if err != nil { + return fmt.Errorf("json.Marshal(series): %w", err) + } + + params := make(url.Values) + params.Add("moveFiles", "true") + + b, err := s.Put(ctx, "v3/series/"+strconv.FormatInt(seriesID, starr.Base10), params, put) + if err != nil { + return fmt.Errorf("api.Put(series): %w", err) + } + + log.Println("SHOW THIS TO CAPTAIN plz:", string(b)) + + return nil +} + +// AddSeries adds a new series to Sonarr. +func (s *Sonarr) AddSeries(series *AddSeriesInput) (*AddSeriesOutput, error) { + return s.AddSeriesContext(context.Background(), series) +} + +func (s *Sonarr) AddSeriesContext(ctx context.Context, series *AddSeriesInput) (*AddSeriesOutput, error) { + body, err := json.Marshal(series) + if err != nil { + return nil, fmt.Errorf("json.Marshal(series): %w", err) + } + + params := make(url.Values) + params.Add("moveFiles", "true") + + var output AddSeriesOutput + if err = s.PostInto(ctx, "v3/series", params, body, &output); err != nil { + return nil, fmt.Errorf("api.Post(series): %w", err) + } + + return &output, nil +} + +// GetSeriesByID locates and returns a series by DB [series] ID. +func (s *Sonarr) GetSeriesByID(seriesID int64) (*Series, error) { + return s.GetSeriesByIDContext(context.Background(), seriesID) +} + +func (s *Sonarr) GetSeriesByIDContext(ctx context.Context, seriesID int64) (*Series, error) { + var series Series + + err := s.GetInto(ctx, "v3/series/"+strconv.FormatInt(seriesID, starr.Base10), nil, &series) + if err != nil { + return nil, fmt.Errorf("api.Get(series): %w", err) + } + + return &series, nil +} + +// GetSeriesLookup searches for a series [in Servarr] using a search term or a tvdbid. +// Provide a search term or a tvdbid. If you provide both, tvdbID is used. +func (s *Sonarr) GetSeriesLookup(term string, tvdbID int64) ([]*Series, error) { + return s.GetSeriesLookupContext(context.Background(), term, tvdbID) +} + +func (s *Sonarr) GetSeriesLookupContext(ctx context.Context, term string, tvdbID int64) ([]*Series, error) { + params := make(url.Values) + + if tvdbID > 0 { + params.Add("term", "tvdbid:"+strconv.FormatInt(tvdbID, starr.Base10)) + } else { + params.Add("term", term) + } + + var series []*Series + + err := s.GetInto(ctx, "v3/series/lookup", params, &series) + if err != nil { + return nil, fmt.Errorf("api.Get(series/lookup): %w", err) + } + + return series, nil +} + +// Lookup will search for series matching the specified search term. +// Searches for new shows on TheTVDB.com utilizing sonarr.tv's caching and augmentation proxy. +func (s *Sonarr) Lookup(term string) ([]*Series, error) { + return s.LookupContext(context.Background(), term) +} + +func (s *Sonarr) LookupContext(ctx context.Context, term string) ([]*Series, error) { + return s.GetSeriesLookupContext(ctx, term, 0) +} diff --git a/sonarr/sonarr.go b/sonarr/sonarr.go index 80a1a49..1dd8404 100644 --- a/sonarr/sonarr.go +++ b/sonarr/sonarr.go @@ -1,15 +1,41 @@ package sonarr import ( - "encoding/json" + "context" + "crypto/tls" "fmt" - "log" - "net/url" - "strconv" + "net/http" "golift.io/starr" ) +// Sonarr contains all the methods to interact with a Sonarr server. +type Sonarr struct { + starr.APIer +} + +// New returns a Sonarr object used to interact with the Sonarr API. +func New(config *starr.Config) *Sonarr { + if config.Client == nil { + //nolint:exhaustivestruct,gosec + config.Client = &http.Client{ + Timeout: config.Timeout.Duration, + CheckRedirect: func(r *http.Request, via []*http.Request) error { + return http.ErrUseLastResponse + }, + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: !config.ValidSSL}, + }, + } + } + + if config.Debugf == nil { + config.Debugf = func(string, ...interface{}) {} + } + + return &Sonarr{APIer: config} +} + // GetQueue returns a single page from the Sonarr Queue (processing, but not yet imported). // WARNING: 12/30/2021 - this method changed. // If you need control over the page, use sonarr.GetQueuePage(). @@ -17,12 +43,16 @@ import ( // up to the number of records present in the application. // It grabs records in (paginated) batches of perPage, and concatenates // them into one list. Passing zero for records will return all of them. -func (s *Sonarr) GetQueue(records, perPage int) (*Queue, error) { //nolint:dupl +func (s *Sonarr) GetQueue(records, perPage int) (*Queue, error) { + return s.GetQueueContext(context.Background(), records, perPage) +} + +func (s *Sonarr) GetQueueContext(ctx context.Context, records, perPage int) (*Queue, error) { queue := &Queue{Records: []*QueueRecord{}} perPage = starr.SetPerPage(records, perPage) for page := 1; ; page++ { - curr, err := s.GetQueuePage(&starr.Req{PageSize: perPage, Page: page}) + curr, err := s.GetQueuePageContext(ctx, &starr.Req{PageSize: perPage, Page: page}) if err != nil { return nil, err } @@ -49,12 +79,16 @@ func (s *Sonarr) GetQueue(records, perPage int) (*Queue, error) { //nolint:dupl // GetQueuePage returns a single page from the Sonarr Queue. // The page size and number is configurable with the input request parameters. func (s *Sonarr) GetQueuePage(params *starr.Req) (*Queue, error) { + return s.GetQueuePageContext(context.Background(), params) +} + +func (s *Sonarr) GetQueuePageContext(ctx context.Context, params *starr.Req) (*Queue, error) { var queue Queue params.CheckSet("sortKey", "timeleft") params.CheckSet("includeUnknownSeriesItems", "true") - err := s.GetInto("v3/queue", params.Params(), &queue) + err := s.GetInto(ctx, "v3/queue", params.Params(), &queue) if err != nil { return nil, fmt.Errorf("api.Get(queue): %w", err) } @@ -62,441 +96,34 @@ func (s *Sonarr) GetQueuePage(params *starr.Req) (*Queue, error) { return &queue, nil } -// GetTags returns all the tags. -func (s *Sonarr) GetTags() ([]*starr.Tag, error) { - var tags []*starr.Tag - - err := s.GetInto("v3/tag", nil, &tags) - if err != nil { - return nil, fmt.Errorf("api.Get(tag): %w", err) - } - - return tags, nil -} - -// UpdateTag updates the label for a tag. -func (s *Sonarr) UpdateTag(tagID int, label string) (int, error) { - body, err := json.Marshal(&starr.Tag{Label: label, ID: tagID}) - if err != nil { - return 0, fmt.Errorf("json.Marshal(tag): %w", err) - } - - var tag starr.Tag - if err = s.PutInto("v3/tag", nil, body, &tag); err != nil { - return tag.ID, fmt.Errorf("api.Put(tag): %w", err) - } - - return tag.ID, nil -} - -// AddTag adds a tag or returns the ID for an existing tag. -func (s *Sonarr) AddTag(label string) (int, error) { - body, err := json.Marshal(&starr.Tag{Label: label, ID: 0}) - if err != nil { - return 0, fmt.Errorf("json.Marshal(tag): %w", err) - } - - var tag starr.Tag - if err = s.PostInto("v3/tag", nil, body, &tag); err != nil { - return tag.ID, fmt.Errorf("api.Post(tag): %w", err) - } - - return tag.ID, nil -} - -// GetSystemStatus returns system status. -func (s *Sonarr) GetSystemStatus() (*SystemStatus, error) { - var status SystemStatus - - err := s.GetInto("v3/system/status", nil, &status) - if err != nil { - return nil, fmt.Errorf("api.Get(system/status): %w", err) - } - - return &status, nil -} - // GetLanguageProfiles returns all configured language profiles. func (s *Sonarr) GetLanguageProfiles() ([]*LanguageProfile, error) { - var profiles []*LanguageProfile - - err := s.GetInto("v3/languageprofile", nil, &profiles) - if err != nil { - return nil, fmt.Errorf("api.Get(languageprofile): %w", err) - } - - return profiles, nil + return s.GetLanguageProfilesContext(context.Background()) } -// GetQualityProfiles returns all configured quality profiles. -func (s *Sonarr) GetQualityProfiles() ([]*QualityProfile, error) { - var profiles []*QualityProfile - - err := s.GetInto("v3/qualityprofile", nil, &profiles) - if err != nil { - return nil, fmt.Errorf("api.Get(qualityprofile): %w", err) - } - - return profiles, nil -} - -// AddQualityProfile updates a quality profile in place. -func (s *Sonarr) AddQualityProfile(profile *QualityProfile) (int64, error) { - post, err := json.Marshal(profile) - if err != nil { - return 0, fmt.Errorf("json.Marshal(profile): %w", err) - } - - var output QualityProfile - - err = s.PostInto("v3/qualityProfile", nil, post, &output) - if err != nil { - return 0, fmt.Errorf("api.Post(qualityProfile): %w", err) - } - - return output.ID, nil -} - -// UpdateQualityProfile updates a quality profile in place. -func (s *Sonarr) UpdateQualityProfile(profile *QualityProfile) error { - put, err := json.Marshal(profile) - if err != nil { - return fmt.Errorf("json.Marshal(profile): %w", err) - } - - _, err = s.Put("v3/qualityProfile/"+strconv.FormatInt(profile.ID, starr.Base10), nil, put) - if err != nil { - return fmt.Errorf("api.Put(qualityProfile): %w", err) - } - - return nil -} - -// GetReleaseProfiles returns all configured release profiles. -func (s *Sonarr) GetReleaseProfiles() ([]*ReleaseProfile, error) { - var profiles []*ReleaseProfile +func (s *Sonarr) GetLanguageProfilesContext(ctx context.Context) ([]*LanguageProfile, error) { + var profiles []*LanguageProfile - err := s.GetInto("v3/releaseProfile", nil, &profiles) + err := s.GetInto(ctx, "v3/languageprofile", nil, &profiles) if err != nil { - return nil, fmt.Errorf("api.Get(releaseProfile): %w", err) + return nil, fmt.Errorf("api.Get(languageprofile): %w", err) } return profiles, nil } -// AddReleaseProfile updates a release profile in place. -func (s *Sonarr) AddReleaseProfile(profile *ReleaseProfile) (int64, error) { - post, err := json.Marshal(profile) - if err != nil { - return 0, fmt.Errorf("json.Marshal(profile): %w", err) - } - - var output ReleaseProfile - - err = s.PostInto("v3/releaseProfile", nil, post, &output) - if err != nil { - return 0, fmt.Errorf("api.Post(releaseProfile): %w", err) - } - - return output.ID, nil -} - -// UpdateReleaseProfile updates a release profile in place. -func (s *Sonarr) UpdateReleaseProfile(profile *ReleaseProfile) error { - put, err := json.Marshal(profile) - if err != nil { - return fmt.Errorf("json.Marshal(profile): %w", err) - } - - _, err = s.Put("v3/releaseProfile/"+strconv.FormatInt(profile.ID, starr.Base10), nil, put) - if err != nil { - return fmt.Errorf("api.Put(releaseProfile): %w", err) - } - - return nil -} - // GetRootFolders returns all configured root folders. func (s *Sonarr) GetRootFolders() ([]*RootFolder, error) { + return s.GetRootFoldersContext(context.Background()) +} + +func (s *Sonarr) GetRootFoldersContext(ctx context.Context) ([]*RootFolder, error) { var folders []*RootFolder - err := s.GetInto("v3/rootfolder", nil, &folders) + err := s.GetInto(ctx, "v3/rootfolder", nil, &folders) if err != nil { return nil, fmt.Errorf("api.Get(rootfolder): %w", err) } return folders, nil } - -// GetSeriesLookup searches for a series [in Servarr] using a search term or a tvdbid. -// Provide a search term or a tvdbid. If you provide both, tvdbID is used. -func (s *Sonarr) GetSeriesLookup(term string, tvdbID int64) ([]*SeriesLookup, error) { - params := make(url.Values) - - if tvdbID > 0 { - params.Add("term", "tvdbid:"+strconv.FormatInt(tvdbID, starr.Base10)) - } else { - params.Add("term", term) - } - - var series []*SeriesLookup - - err := s.GetInto("v3/series/lookup", params, &series) - if err != nil { - return nil, fmt.Errorf("api.Get(series/lookup): %w", err) - } - - return series, nil -} - -// GetSeries locates and returns a series by tvdbID. If tvdbID is 0, returns all series. -func (s *Sonarr) GetSeries(tvdbID int64) ([]*Series, error) { - params := make(url.Values) - - if tvdbID != 0 { - params.Add("tvdbId", strconv.FormatInt(tvdbID, starr.Base10)) - } - - var series []*Series - - err := s.GetInto("v3/series", params, &series) - if err != nil { - return nil, fmt.Errorf("api.Get(series): %w", err) - } - - return series, nil -} - -// GetSeriesByID locates and returns a series by DB [series] ID. -func (s *Sonarr) GetSeriesByID(seriesID int64) (*Series, error) { - var series Series - - err := s.GetInto("v3/series/"+strconv.FormatInt(seriesID, starr.Base10), nil, &series) - if err != nil { - return nil, fmt.Errorf("api.Get(series): %w", err) - } - - return &series, nil -} - -// GetAllSeries returns all configured series. -// This may not deal well with pagination atm. -func (s *Sonarr) GetAllSeries() ([]*Series, error) { - return s.GetSeries(0) -} - -// UpdateSeries updates a series in place. -func (s *Sonarr) UpdateSeries(seriesID int64, series *Series) error { - put, err := json.Marshal(series) - if err != nil { - return fmt.Errorf("json.Marshal(series): %w", err) - } - - params := make(url.Values) - params.Add("moveFiles", "true") - - b, err := s.Put("v3/series/"+strconv.FormatInt(seriesID, starr.Base10), params, put) - if err != nil { - return fmt.Errorf("api.Put(series): %w", err) - } - - log.Println("SHOW THIS TO CAPTAIN plz:", string(b)) - - return nil -} - -// AddSeries adds a new series to Sonarr. -func (s *Sonarr) AddSeries(series *AddSeriesInput) (*AddSeriesOutput, error) { - body, err := json.Marshal(series) - if err != nil { - return nil, fmt.Errorf("json.Marshal(series): %w", err) - } - - params := make(url.Values) - params.Add("moveFiles", "true") - - var output AddSeriesOutput - if err = s.PostInto("v3/series", params, body, &output); err != nil { - return nil, fmt.Errorf("api.Post(series): %w", err) - } - - return &output, nil -} - -// GetCommands returns all available Sonarr commands. -// These can be used with SendCommand. -func (s *Sonarr) GetCommands() ([]*CommandResponse, error) { - var output []*CommandResponse - - if err := s.GetInto("v3/command", nil, &output); err != nil { - return nil, fmt.Errorf("api.Get(command): %w", err) - } - - return output, nil -} - -// SendCommand sends a command to Sonarr. -func (s *Sonarr) SendCommand(cmd *CommandRequest) (*CommandResponse, error) { - var output CommandResponse - - if cmd == nil || cmd.Name == "" { - return &output, nil - } - - body, err := json.Marshal(cmd) - if err != nil { - return nil, fmt.Errorf("json.Marshal(cmd): %w", err) - } - - if err := s.PostInto("v3/command", nil, body, &output); err != nil { - return nil, fmt.Errorf("api.Post(command): %w", err) - } - - return &output, nil -} - -// GetCommandStatus returns the status of an already started command. -func (s *Sonarr) GetCommandStatus(commandID int64) (*CommandResponse, error) { - var output CommandResponse - - if commandID == 0 { - return &output, nil - } - - err := s.GetInto("v3/command/"+strconv.FormatInt(commandID, starr.Base10), nil, &output) - if err != nil { - return nil, fmt.Errorf("api.Post(command): %w", err) - } - - return &output, nil -} - -// GetSeriesEpisodes returns all episodes for a series by series ID. -// You can get series IDs from GetAllSeries() and GetSeries(). -func (s *Sonarr) GetSeriesEpisodes(seriesID int64) ([]*Episode, error) { - var output []*Episode - - params := make(url.Values) - params.Add("seriesId", strconv.FormatInt(seriesID, starr.Base10)) - - err := s.GetInto("v3/episode?seriesId", params, &output) - if err != nil { - return nil, fmt.Errorf("api.Get(episode): %w", err) - } - - return output, nil -} - -// MonitorEpisode sends a request to monitor (true) or unmonitor (false) a list of episodes by ID. -// You can get episode IDs from GetSeriesEpisodes(). -func (s *Sonarr) MonitorEpisode(episodeIDs []int64, monitor bool) ([]*Episode, error) { - var ( - input, _ = json.Marshal(&struct { - E []int64 `json:"episodeIds"` - M bool `json:"monitored"` - }{E: episodeIDs, M: monitor}) - output []*Episode - ) - - if err := s.PutInto("v3/episode/monitor", nil, input, &output); err != nil { - return nil, fmt.Errorf("api.Put(episode/monitor): %w", err) - } - - return output, nil -} - -// GetHistory returns the Sonarr History (grabs/failures/completed). -// WARNING: 12/30/2021 - this method changed. -// If you need control over the page, use sonarr.GetHistoryPage(). -// This function simply returns the number of history records desired, -// up to the number of records present in the application. -// It grabs records in (paginated) batches of perPage, and concatenates -// them into one list. Passing zero for records will return all of them. -func (s *Sonarr) GetHistory(records, perPage int) (*History, error) { //nolint:dupl - hist := &History{Records: []*HistoryRecord{}} - perPage = starr.SetPerPage(records, perPage) - - for page := 1; ; page++ { - curr, err := s.GetHistoryPage(&starr.Req{PageSize: perPage, Page: page}) - if err != nil { - return nil, err - } - - hist.Records = append(hist.Records, curr.Records...) - - if len(hist.Records) >= curr.TotalRecords || - (len(hist.Records) >= records && records != 0) || - len(curr.Records) == 0 { - hist.PageSize = curr.TotalRecords - hist.TotalRecords = curr.TotalRecords - hist.SortDirection = curr.SortDirection - hist.SortKey = curr.SortKey - - break - } - - perPage = starr.AdjustPerPage(records, curr.TotalRecords, len(hist.Records), perPage) - } - - return hist, nil -} - -// GetHistoryPage returns a single page from the Sonarr History (grabs/failures/completed). -// The page size and number is configurable with the input request parameters. -func (s *Sonarr) GetHistoryPage(params *starr.Req) (*History, error) { - var history History - - err := s.GetInto("v3/history", params.Params(), &history) - if err != nil { - return nil, fmt.Errorf("api.Get(history): %w", err) - } - - return &history, nil -} - -// Fail marks the given history item as failed by id. -func (s *Sonarr) Fail(historyID int64) error { - if historyID < 1 { - return fmt.Errorf("%w: invalid history ID: %d", starr.ErrRequestError, historyID) - } - - // Strangely uses a POST without a payload. - _, err := s.Post("v3/history/failed/"+strconv.FormatInt(historyID, starr.Base10), nil, nil) - if err != nil { - return fmt.Errorf("api.Post(history/failed): %w", err) - } - - return nil -} - -// Lookup will search for series matching the specified search term. -// Searches for new shows on TheTVDB.com utilizing sonarr.tv's caching and augmentation proxy. -func (s *Sonarr) Lookup(term string) ([]*Series, error) { - var output []*Series - - if term == "" { - return output, nil - } - - params := make(url.Values) - params.Set("term", term) - - err := s.GetInto("v3/series/lookup", params, &output) - if err != nil { - return nil, fmt.Errorf("api.Get(series/lookup): %w", err) - } - - return output, nil -} - -// GetBackupFiles returns all available Sonarr backup files. -// Use GetBody to download a file using BackupFile.Path. -func (s *Sonarr) GetBackupFiles() ([]*starr.BackupFile, error) { - var output []*starr.BackupFile - - if err := s.GetInto("v3/system/backup", nil, &output); err != nil { - return nil, fmt.Errorf("api.Get(system/backup): %w", err) - } - - return output, nil -} diff --git a/sonarr/system.go b/sonarr/system.go new file mode 100644 index 0000000..d18d475 --- /dev/null +++ b/sonarr/system.go @@ -0,0 +1,40 @@ +package sonarr + +import ( + "context" + "fmt" + + "golift.io/starr" +) + +// GetSystemStatus returns system status. +func (s *Sonarr) GetSystemStatus() (*SystemStatus, error) { + return s.GetSystemStatusContext(context.Background()) +} + +func (s *Sonarr) GetSystemStatusContext(ctx context.Context) (*SystemStatus, error) { + var status SystemStatus + + err := s.GetInto(ctx, "v3/system/status", nil, &status) + if err != nil { + return nil, fmt.Errorf("api.Get(system/status): %w", err) + } + + return &status, nil +} + +// GetBackupFiles returns all available Sonarr backup files. +// Use GetBody to download a file using BackupFile.Path. +func (s *Sonarr) GetBackupFiles() ([]*starr.BackupFile, error) { + return s.GetBackupFilesContext(context.Background()) +} + +func (s *Sonarr) GetBackupFilesContext(ctx context.Context) ([]*starr.BackupFile, error) { + var output []*starr.BackupFile + + if err := s.GetInto(ctx, "v3/system/backup", nil, &output); err != nil { + return nil, fmt.Errorf("api.Get(system/backup): %w", err) + } + + return output, nil +} diff --git a/sonarr/tag.go b/sonarr/tag.go new file mode 100644 index 0000000..a07b1c6 --- /dev/null +++ b/sonarr/tag.go @@ -0,0 +1,63 @@ +package sonarr + +import ( + "context" + "encoding/json" + "fmt" + + "golift.io/starr" +) + +// GetTags returns all the tags. +func (s *Sonarr) GetTags() ([]*starr.Tag, error) { + return s.GetTagsContext(context.Background()) +} + +func (s *Sonarr) GetTagsContext(ctx context.Context) ([]*starr.Tag, error) { + var tags []*starr.Tag + + err := s.GetInto(ctx, "v3/tag", nil, &tags) + if err != nil { + return nil, fmt.Errorf("api.Get(tag): %w", err) + } + + return tags, nil +} + +// UpdateTag updates the label for a tag. +func (s *Sonarr) UpdateTag(tagID int, label string) (int, error) { + return s.UpdateTagContext(context.Background(), tagID, label) +} + +func (s *Sonarr) UpdateTagContext(ctx context.Context, tagID int, label string) (int, error) { + body, err := json.Marshal(&starr.Tag{Label: label, ID: tagID}) + if err != nil { + return 0, fmt.Errorf("json.Marshal(tag): %w", err) + } + + var tag starr.Tag + if err = s.PutInto(ctx, "v3/tag", nil, body, &tag); err != nil { + return tag.ID, fmt.Errorf("api.Put(tag): %w", err) + } + + return tag.ID, nil +} + +// AddTag adds a tag or returns the ID for an existing tag. +func (s *Sonarr) AddTag(label string) (int, error) { + return s.AddTagContext(context.Background(), label) +} + +func (s *Sonarr) AddTagContext(ctx context.Context, label string) (int, error) { + body, err := json.Marshal(&starr.Tag{Label: label, ID: 0}) + if err != nil { + return 0, fmt.Errorf("json.Marshal(tag): %w", err) + } + + var tag starr.Tag + if err = s.PostInto(ctx, "v3/tag", nil, body, &tag); err != nil { + return tag.ID, fmt.Errorf("api.Post(tag): %w", err) + } + + return tag.ID, nil +} diff --git a/sonarr/type.go b/sonarr/type.go index bc360e2..f3a5a58 100644 --- a/sonarr/type.go +++ b/sonarr/type.go @@ -1,40 +1,11 @@ package sonarr import ( - "crypto/tls" - "net/http" "time" "golift.io/starr" ) -// Sonarr contains all the methods to interact with a Sonarr server. -type Sonarr struct { - starr.APIer -} - -// New returns a Sonarr object used to interact with the Sonarr API. -func New(config *starr.Config) *Sonarr { - if config.Client == nil { - //nolint:exhaustivestruct,gosec - config.Client = &http.Client{ - Timeout: config.Timeout.Duration, - CheckRedirect: func(r *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - }, - Transport: &http.Transport{ - TLSClientConfig: &tls.Config{InsecureSkipVerify: !config.ValidSSL}, - }, - } - } - - if config.Debugf == nil { - config.Debugf = func(string, ...interface{}) {} - } - - return &Sonarr{APIer: config} -} - // QualityProfile is the /api/v3/qualityprofile endpoint. type QualityProfile struct { ID int64 `json:"id"` @@ -205,42 +176,6 @@ type Episode struct { Monitored bool `json:"monitored"` } -// SeriesLookup is the /api/v3/series/lookup endpoint. -type SeriesLookup struct { - Title string `json:"title"` - SortTitle string `json:"sortTitle"` - Status string `json:"status"` - Overview string `json:"overview"` - Network string `json:"network"` - AirTime string `json:"airTime"` - Images []*starr.Image `json:"images"` - RemotePoster string `json:"remotePoster"` - Seasons []*Season `json:"seasons"` - Year int `json:"year"` - QualityProfileID int64 `json:"qualityProfileId"` - LanguageProfileID int64 `json:"languageProfileId"` - Runtime int `json:"runtime"` - TvdbID int64 `json:"tvdbId"` - TvRageID int64 `json:"tvRageId"` - TvMazeID int64 `json:"tvMazeId"` - FirstAired time.Time `json:"firstAired"` - SeriesType string `json:"seriesType"` - CleanTitle string `json:"cleanTitle"` - ImdbID string `json:"imdbId"` - TitleSlug string `json:"titleSlug"` - Folder string `json:"folder"` - Certification string `json:"certification"` - Genres []string `json:"genres"` - Tags []int `json:"tags"` - Added time.Time `json:"added"` - Ratings *starr.Ratings `json:"ratings"` - Statistics *Statistics `json:"statistics"` - Ended bool `json:"ended"` - SeasonFolder bool `json:"seasonFolder"` - Monitored bool `json:"monitored"` - UseSceneNumbering bool `json:"useSceneNumbering"` -} - // LanguageProfile is the /api/v3/languageprofile endpoint. type LanguageProfile struct { Name string `json:"name"` @@ -261,7 +196,7 @@ type AddSeriesInput struct { ID int64 `json:"id,omitempty"` TvdbID int64 `json:"tvdbId"` QualityProfileID int64 `json:"qualityProfileId"` - LanguageProfileID int64 `json:"languageProfileID"` + LanguageProfileID int64 `json:"languageProfileId"` Tags []int `json:"tags"` RootFolderPath string `json:"rootFolderPath"` Title string `json:"title,omitempty"`