Skip to content

Commit

Permalink
Update main Interface with Context-aware methods. (#24)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
davidnewhall authored Jan 15, 2022
1 parent 44b7a8e commit cd5c2c4
Show file tree
Hide file tree
Showing 48 changed files with 2,803 additions and 1,861 deletions.
10 changes: 5 additions & 5 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 ./...
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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.
)
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
69 changes: 31 additions & 38 deletions http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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)))
Expand All @@ -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 {
Expand Down
81 changes: 45 additions & 36 deletions interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand All @@ -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)
}
Expand Down
Loading

0 comments on commit cd5c2c4

Please sign in to comment.