feat(logging); improve messages and errors (#336)

* feat(logger): add module context

* feat(logger): change errors package

* feat(logger): update tests
This commit is contained in:
Ludvig Lundgren 2022-07-05 13:31:44 +02:00 committed by GitHub
parent 95471a4cf7
commit 0e88117702
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
69 changed files with 1172 additions and 957 deletions

View file

@ -1,21 +1,20 @@
package btn
import (
"fmt"
"github.com/autobrr/autobrr/internal/domain"
"github.com/autobrr/autobrr/pkg/errors"
)
func (c *Client) TestAPI() (bool, error) {
res, err := c.rpcClient.Call("userInfo", [2]string{c.APIKey})
if err != nil {
return false, err
return false, errors.Wrap(err, "test api userInfo failed")
}
var u *UserInfo
err = res.GetObject(&u)
if err != nil {
return false, err
return false, errors.Wrap(err, "test api get userInfo")
}
if u.Username != "" {
@ -27,12 +26,12 @@ func (c *Client) TestAPI() (bool, error) {
func (c *Client) GetTorrentByID(torrentID string) (*domain.TorrentBasic, error) {
if torrentID == "" {
return nil, fmt.Errorf("btn client: must have torrentID")
return nil, errors.New("btn client: must have torrentID")
}
res, err := c.rpcClient.Call("getTorrentById", [2]string{c.APIKey, torrentID})
if err != nil {
return nil, err
return nil, errors.Wrap(err, "call getTorrentById failed")
}
var r *domain.TorrentBasic

View file

@ -2,10 +2,13 @@ package btn
import (
"context"
"io"
"log"
"net/http"
"time"
"github.com/autobrr/autobrr/internal/domain"
"github.com/autobrr/autobrr/pkg/errors"
"github.com/autobrr/autobrr/pkg/jsonrpc"
"golang.org/x/time/rate"
@ -23,6 +26,8 @@ type Client struct {
Ratelimiter *rate.Limiter
APIKey string
Headers http.Header
Log *log.Logger
}
func NewClient(url string, apiKey string) BTNClient {
@ -41,6 +46,10 @@ func NewClient(url string, apiKey string) BTNClient {
Ratelimiter: rate.NewLimiter(rate.Every(150*time.Hour), 1), // 150 rpcRequest every 1 hour
}
if c.Log == nil {
c.Log = log.New(io.Discard, "", log.LstdFlags)
}
return c
}
@ -48,11 +57,11 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
ctx := context.Background()
err := c.Ratelimiter.Wait(ctx) // This is a blocking call. Honors the rate limit
if err != nil {
return nil, err
return nil, errors.Wrap(err, "error waiting for ratelimiter")
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "could not make request")
}
return resp, nil
}

171
pkg/errors/errors.go Normal file
View file

@ -0,0 +1,171 @@
package errors
import (
"fmt"
"reflect"
"runtime"
"unsafe"
"github.com/pkg/errors"
)
// Export a number of functions or variables from pkg/errors. We want people to be able to
// use them, if only via the entrypoints we've vetted in this file.
var (
As = errors.As
Is = errors.Is
Cause = errors.Cause
Unwrap = errors.Unwrap
)
// StackTrace should be aliases rather than newtype'd, so it can work with any of the
// functions we export from pkg/errors.
type StackTrace = errors.StackTrace
type StackTracer interface {
StackTrace() errors.StackTrace
}
// Sentinel is used to create compile-time errors that are intended to be value only, with
// no associated stack trace.
func Sentinel(msg string, args ...interface{}) error {
return fmt.Errorf(msg, args...)
}
// New acts as pkg/errors.New does, producing a stack traced error, but supports
// interpolating of message parameters. Use this when you want the stack trace to start at
// the place you create the error.
func New(msg string, args ...interface{}) error {
return PopStack(errors.New(fmt.Sprintf(msg, args...)))
}
// Wrap creates a new error from a cause, decorating the original error message with a
// prefix.
//
// It differs from the pkg/errors Wrap/Wrapf by idempotently creating a stack trace,
// meaning we won't create another stack trace when there is already a stack trace present
// that matches our current program position.
func Wrap(cause error, msg string, args ...interface{}) error {
causeStackTracer := new(StackTracer)
if errors.As(cause, causeStackTracer) {
// If our cause has set a stack trace, and that trace is a child of our own function
// as inferred by prefix matching our current program counter stack, then we only want
// to decorate the error message rather than add a redundant stack trace.
if ancestorOfCause(callers(1), (*causeStackTracer).StackTrace()) {
return errors.WithMessagef(cause, msg, args...) // no stack added, no pop required
}
}
// Otherwise we can't see a stack trace that represents ourselves, so let's add one.
return PopStack(errors.Wrapf(cause, msg, args...))
}
// ancestorOfCause returns true if the caller looks to be an ancestor of the given stack
// trace. We check this by seeing whether our stack prefix-matches the cause stack, which
// should imply the error was generated directly from our goroutine.
func ancestorOfCause(ourStack []uintptr, causeStack errors.StackTrace) bool {
// Stack traces are ordered such that the deepest frame is first. We'll want to check
// for prefix matching in reverse.
//
// As an example, imagine we have a prefix-matching stack for ourselves:
// [
// "github.com/onsi/ginkgo/internal/leafnodes.(*runner).runSync",
// "github.com/incident-io/core/server/pkg/errors_test.TestSuite",
// "testing.tRunner",
// "runtime.goexit"
// ]
//
// We'll want to compare this against an error cause that will have happened further
// down the stack. An example stack trace from such an error might be:
// [
// "github.com/incident-io/core/server/pkg/errors.New",
// "github.com/incident-io/core/server/pkg/errors_test.glob..func1.2.2.2.1",,
// "github.com/onsi/ginkgo/internal/leafnodes.(*runner).runSync",
// "github.com/incident-io/core/server/pkg/errors_test.TestSuite",
// "testing.tRunner",
// "runtime.goexit"
// ]
//
// They prefix match, but we'll have to handle the match carefully as we need to match
// from back to forward.
// We can't possibly prefix match if our stack is larger than the cause stack.
if len(ourStack) > len(causeStack) {
return false
}
// We know the sizes are compatible, so compare program counters from back to front.
for idx := 0; idx < len(ourStack); idx++ {
if ourStack[len(ourStack)-1] != (uintptr)(causeStack[len(causeStack)-1]) {
return false
}
}
// All comparisons checked out, these stacks match
return true
}
func callers(skip int) []uintptr {
pc := make([]uintptr, 32) // assume we'll have at most 32 frames
n := runtime.Callers(skip+3, pc) // capture those frames, skipping runtime.Callers, ourself and the calling function
return pc[:n] // return everything that we captured
}
// RecoverPanic turns a panic into an error, adjusting the stacktrace so it originates at
// the line that caused it.
//
// Example:
//
// func Do() (err error) {
// defer func() {
// errors.RecoverPanic(recover(), &err)
// }()
// }
func RecoverPanic(r interface{}, errPtr *error) {
var err error
if r != nil {
if panicErr, ok := r.(error); ok {
err = errors.Wrap(panicErr, "caught panic")
} else {
err = errors.New(fmt.Sprintf("caught panic: %v", r))
}
}
if err != nil {
// Pop twice: once for the errors package, then again for the defer function we must
// run this under. We want the stacktrace to originate at the source of the panic, not
// in the infrastructure that catches it.
err = PopStack(err) // errors.go
err = PopStack(err) // defer
*errPtr = err
}
}
// PopStack removes the top of the stack from an errors stack trace.
func PopStack(err error) error {
if err == nil {
return err
}
// We want to remove us, the internal/errors.New function, from the error stack we just
// produced. There's no official way of reaching into the error and adjusting this, as
// the stack is stored as a private field on an unexported struct.
//
// This does some unsafe badness to adjust that field, which should not be repeated
// anywhere else.
stackField := reflect.ValueOf(err).Elem().FieldByName("stack")
if stackField.IsZero() {
return err
}
stackFieldPtr := (**[]uintptr)(unsafe.Pointer(stackField.UnsafeAddr()))
// Remove the first of the frames, dropping 'us' from the error stack trace.
frames := (**stackFieldPtr)[1:]
// Assign to the internal stack field
*stackFieldPtr = &frames
return err
}

View file

@ -3,7 +3,6 @@ package ggn
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
@ -12,6 +11,7 @@ import (
"time"
"github.com/autobrr/autobrr/internal/domain"
"github.com/autobrr/autobrr/pkg/errors"
"golang.org/x/time/rate"
)
@ -147,11 +147,11 @@ func (c *client) Do(req *http.Request) (*http.Response, error) {
ctx := context.Background()
err := c.Ratelimiter.Wait(ctx) // This is a blocking call. Honors the rate limit
if err != nil {
return nil, err
return nil, errors.Wrap(err, "error waiting for ratelimiter")
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "error making request")
}
return resp, nil
}
@ -159,7 +159,7 @@ func (c *client) Do(req *http.Request) (*http.Response, error) {
func (c *client) get(url string) (*http.Response, error) {
req, err := http.NewRequest(http.MethodGet, url, http.NoBody)
if err != nil {
return nil, errors.New(fmt.Sprintf("ggn client request error : %v", url))
return nil, errors.Wrap(err, "ggn client request error : %v", url)
}
req.Header.Add("X-API-Key", c.APIKey)
@ -167,7 +167,7 @@ func (c *client) get(url string) (*http.Response, error) {
res, err := c.Do(req)
if err != nil {
return nil, errors.New(fmt.Sprintf("ggn client request error : %v", url))
return nil, errors.Wrap(err, "ggn client request error : %v", url)
}
if res.StatusCode == http.StatusUnauthorized {
@ -183,7 +183,7 @@ func (c *client) get(url string) (*http.Response, error) {
func (c *client) GetTorrentByID(torrentID string) (*domain.TorrentBasic, error) {
if torrentID == "" {
return nil, fmt.Errorf("ggn client: must have torrentID")
return nil, errors.New("ggn client: must have torrentID")
}
var r Response
@ -192,27 +192,27 @@ func (c *client) GetTorrentByID(torrentID string) (*domain.TorrentBasic, error)
v.Add("id", torrentID)
params := v.Encode()
url := fmt.Sprintf("%v?%v&%v", c.Url, "request=torrent", params)
reqUrl := fmt.Sprintf("%v?%v&%v", c.Url, "request=torrent", params)
resp, err := c.get(url)
resp, err := c.get(reqUrl)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "error getting data")
}
defer resp.Body.Close()
body, readErr := ioutil.ReadAll(resp.Body)
if readErr != nil {
return nil, readErr
return nil, errors.Wrap(readErr, "error reading body")
}
err = json.Unmarshal(body, &r)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "error unmarshal body")
}
if r.Status != "success" {
return nil, fmt.Errorf("bad status: %v", r.Status)
return nil, errors.New("bad status: %v", r.Status)
}
t := &domain.TorrentBasic{
@ -229,7 +229,7 @@ func (c *client) GetTorrentByID(torrentID string) (*domain.TorrentBasic, error)
func (c *client) TestAPI() (bool, error) {
resp, err := c.get(c.Url)
if err != nil {
return false, err
return false, errors.Wrap(err, "error getting data")
}
defer resp.Body.Close()

View file

@ -7,6 +7,8 @@ import (
"net/http"
"reflect"
"strconv"
"github.com/autobrr/autobrr/pkg/errors"
)
type Client interface {
@ -110,12 +112,12 @@ func (c *rpcClient) Call(method string, params ...interface{}) (*RPCResponse, er
func (c *rpcClient) newRequest(req interface{}) (*http.Request, error) {
body, err := json.Marshal(req)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "could not marshal request")
}
request, err := http.NewRequest("POST", c.endpoint, bytes.NewReader(body))
if err != nil {
return nil, err
return nil, errors.Wrap(err, "error creating request")
}
request.Header.Set("Content-Type", "application/json")
request.Header.Set("Accept", "application/json")
@ -131,12 +133,12 @@ func (c *rpcClient) doCall(request RPCRequest) (*RPCResponse, error) {
httpRequest, err := c.newRequest(request)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "could not create rpc http request")
}
httpResponse, err := c.httpClient.Do(httpRequest)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "error during rpc http request")
}
defer httpResponse.Body.Close()
@ -149,7 +151,7 @@ func (c *rpcClient) doCall(request RPCRequest) (*RPCResponse, error) {
if err != nil {
if httpResponse.StatusCode >= 400 {
return nil, fmt.Errorf("rpc call %v() on %v status code: %v. Could not decode body to rpc response: %v", request.Method, httpRequest.URL.String(), httpResponse.StatusCode, err.Error())
return nil, errors.Wrap(err, fmt.Sprintf("rpc call %v() on %v status code: %v. Could not decode body to rpc response", request.Method, httpRequest.URL.String(), httpResponse.StatusCode))
}
// if res.StatusCode == http.StatusUnauthorized {
// return nil, errors.New("unauthorized: bad credentials")
@ -167,7 +169,7 @@ func (c *rpcClient) doCall(request RPCRequest) (*RPCResponse, error) {
}
if rpcResponse == nil {
return nil, fmt.Errorf("rpc call %v() on %v status code: %v. rpc response missing", request.Method, httpRequest.URL.String(), httpResponse.StatusCode)
return nil, errors.New("rpc call %v() on %v status code: %v. rpc response missing", request.Method, httpRequest.URL.String(), httpResponse.StatusCode)
}
return rpcResponse, nil
@ -220,12 +222,12 @@ func Params(params ...interface{}) interface{} {
func (r *RPCResponse) GetObject(toType interface{}) error {
js, err := json.Marshal(r.Result)
if err != nil {
return err
return errors.Wrap(err, "could not marshal object")
}
err = json.Unmarshal(js, toType)
if err != nil {
return err
return errors.Wrap(err, "could not unmarshal object")
}
return nil

View file

@ -3,12 +3,12 @@ package lidarr
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path"
"github.com/autobrr/autobrr/pkg/errors"
)
func (c *client) get(endpoint string) (int, []byte, error) {
@ -18,7 +18,7 @@ func (c *client) get(endpoint string) (int, []byte, error) {
req, err := http.NewRequest(http.MethodGet, reqUrl, http.NoBody)
if err != nil {
return 0, nil, errors.New(fmt.Sprintf("lidarr client request error : %v", reqUrl))
return 0, nil, errors.Wrap(err, "lidarr client request error : %v", reqUrl)
}
if c.config.BasicAuth {
@ -29,14 +29,14 @@ func (c *client) get(endpoint string) (int, []byte, error) {
resp, err := c.http.Do(req)
if err != nil {
return 0, nil, fmt.Errorf("lidarr.http.Do(req): %w", err)
return 0, nil, errors.Wrap(err, "lidarr.http.Do(req)")
}
defer resp.Body.Close()
var buf bytes.Buffer
if _, err = io.Copy(&buf, resp.Body); err != nil {
return resp.StatusCode, nil, fmt.Errorf("lidarr.io.Copy: %w", err)
return resp.StatusCode, nil, errors.Wrap(err, "lidarr.io.Copy error")
}
return resp.StatusCode, buf.Bytes(), nil
@ -49,12 +49,12 @@ func (c *client) post(endpoint string, data interface{}) (*http.Response, error)
jsonData, err := json.Marshal(data)
if err != nil {
return nil, fmt.Errorf("lidarr client could not marshal data: %v", reqUrl)
return nil, errors.Wrap(err, "lidarr client could not marshal data: %v", reqUrl)
}
req, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("lidarr client request error: %v", reqUrl)
return nil, errors.Wrap(err, "lidarr client request error: %v", reqUrl)
}
if c.config.BasicAuth {
@ -67,7 +67,7 @@ func (c *client) post(endpoint string, data interface{}) (*http.Response, error)
res, err := c.http.Do(req)
if err != nil {
return nil, fmt.Errorf("lidarr client request error: %v", reqUrl)
return nil, errors.Wrap(err, "lidarr client request error: %v", reqUrl)
}
// validate response
@ -88,12 +88,12 @@ func (c *client) postBody(endpoint string, data interface{}) (int, []byte, error
jsonData, err := json.Marshal(data)
if err != nil {
return 0, nil, fmt.Errorf("lidarr client could not marshal data: %v", reqUrl)
return 0, nil, errors.Wrap(err, "lidarr client could not marshal data: %v", reqUrl)
}
req, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(jsonData))
if err != nil {
return 0, nil, fmt.Errorf("lidarr client request error: %v", reqUrl)
return 0, nil, errors.Wrap(err, "lidarr client request error: %v", reqUrl)
}
if c.config.BasicAuth {
@ -104,18 +104,20 @@ func (c *client) postBody(endpoint string, data interface{}) (int, []byte, error
resp, err := c.http.Do(req)
if err != nil {
return 0, nil, fmt.Errorf("lidarr.http.Do(req): %w", err)
return 0, nil, errors.Wrap(err, "lidarr.http.Do(req)")
}
defer resp.Body.Close()
var buf bytes.Buffer
if _, err = io.Copy(&buf, resp.Body); err != nil {
return resp.StatusCode, nil, fmt.Errorf("lidarr.io.Copy: %w", err)
return resp.StatusCode, nil, errors.Wrap(err, "lidarr.io.Copy")
}
if resp.StatusCode < 200 || resp.StatusCode > 299 {
return resp.StatusCode, buf.Bytes(), fmt.Errorf("lidarr: bad request: %v (status: %s): %s", resp.Request.RequestURI, resp.Status, buf.String())
if resp.StatusCode == http.StatusBadRequest {
return resp.StatusCode, buf.Bytes(), nil
} else if resp.StatusCode < 200 || resp.StatusCode > 401 {
return resp.StatusCode, buf.Bytes(), errors.New("lidarr: bad request: %v (status: %s): %s", resp.Request.RequestURI, resp.Status, buf.String())
}
return resp.StatusCode, buf.Bytes(), nil

View file

@ -2,11 +2,14 @@ package lidarr
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
"github.com/autobrr/autobrr/pkg/errors"
)
type Config struct {
@ -17,6 +20,8 @@ type Config struct {
BasicAuth bool
Username string
Password string
Log *log.Logger
}
type Client interface {
@ -27,6 +32,8 @@ type Client interface {
type client struct {
config Config
http *http.Client
Log *log.Logger
}
// New create new lidarr client
@ -39,6 +46,11 @@ func New(config Config) Client {
c := &client{
config: config,
http: httpClient,
Log: config.Log,
}
if config.Log == nil {
c.Log = log.New(io.Discard, "", log.LstdFlags)
}
return c
@ -61,6 +73,13 @@ type PushResponse struct {
Rejections []string `json:"rejections"`
}
type BadRequestResponse struct {
PropertyName string `json:"propertyName"`
ErrorMessage string `json:"errorMessage"`
AttemptedValue string `json:"attemptedValue"`
Severity string `json:"severity"`
}
type SystemStatusResponse struct {
Version string `json:"version"`
}
@ -68,43 +87,56 @@ type SystemStatusResponse struct {
func (c *client) Test() (*SystemStatusResponse, error) {
status, res, err := c.get("system/status")
if err != nil {
return nil, fmt.Errorf("lidarr client get error: %w", err)
return nil, errors.Wrap(err, "lidarr client get error")
}
if status == http.StatusUnauthorized {
return nil, errors.New("unauthorized: bad credentials")
}
//log.Trace().Msgf("lidarr system/status response status: %v body: %v", status, string(res))
c.Log.Printf("lidarr system/status response status: %v body: %v", status, string(res))
response := SystemStatusResponse{}
err = json.Unmarshal(res, &response)
if err != nil {
return nil, fmt.Errorf("lidarr client error json unmarshal: %w", err)
return nil, errors.Wrap(err, "lidarr client error json unmarshal")
}
return &response, nil
}
func (c *client) Push(release Release) ([]string, error) {
_, res, err := c.postBody("release/push", release)
status, res, err := c.postBody("release/push", release)
if err != nil {
return nil, fmt.Errorf("lidarr client post error: %w", err)
return nil, errors.Wrap(err, "lidarr client post error")
}
//log.Trace().Msgf("lidarr release/push response status: %v body: %v", status, string(res))
c.Log.Printf("lidarr release/push response status: %v body: %v", status, string(res))
if status == http.StatusBadRequest {
badreqResponse := make([]*BadRequestResponse, 0)
err = json.Unmarshal(res, &badreqResponse)
if err != nil {
return nil, errors.Wrap(err, "could not unmarshal data")
}
if badreqResponse[0] != nil && badreqResponse[0].PropertyName == "Title" && badreqResponse[0].ErrorMessage == "Unable to parse" {
rejections := []string{fmt.Sprintf("unable to parse: %v", badreqResponse[0].AttemptedValue)}
return rejections, err
}
}
pushResponse := PushResponse{}
err = json.Unmarshal(res, &pushResponse)
if err != nil {
return nil, fmt.Errorf("lidarr client error json unmarshal: %w", err)
return nil, errors.Wrap(err, "lidarr client error json unmarshal")
}
// log and return if rejected
if pushResponse.Rejected {
rejections := strings.Join(pushResponse.Rejections, ", ")
return pushResponse.Rejections, fmt.Errorf("lidarr push rejected: %s - reasons: %q: err %w", release.Title, rejections, err)
return pushResponse.Rejections, errors.New("lidarr push rejected: %s - reasons: %q", release.Title, rejections)
}
return nil, nil

View file

@ -1,7 +1,6 @@
package lidarr
import (
"errors"
"io/ioutil"
"net/http"
"net/http/httptest"
@ -134,11 +133,11 @@ func Test_client_Test(t *testing.T) {
defer srv.Close()
tests := []struct {
name string
cfg Config
want *SystemStatusResponse
err error
wantErr bool
name string
cfg Config
want *SystemStatusResponse
expectedErr string
wantErr bool
}{
{
name: "fetch",
@ -149,9 +148,9 @@ func Test_client_Test(t *testing.T) {
Username: "",
Password: "",
},
want: &SystemStatusResponse{Version: "0.8.1.2135"},
err: nil,
wantErr: false,
want: &SystemStatusResponse{Version: "0.8.1.2135"},
expectedErr: "",
wantErr: false,
},
{
name: "fetch_unauthorized",
@ -162,9 +161,9 @@ func Test_client_Test(t *testing.T) {
Username: "",
Password: "",
},
want: nil,
wantErr: true,
err: errors.New("unauthorized: bad credentials"),
want: nil,
wantErr: true,
expectedErr: "unauthorized: bad credentials",
},
}
for _, tt := range tests {
@ -173,7 +172,7 @@ func Test_client_Test(t *testing.T) {
got, err := c.Test()
if tt.wantErr && assert.Error(t, err) {
assert.Equal(t, tt.err, err)
assert.EqualErrorf(t, err, tt.expectedErr, "Error should be: %v, got: %v", tt.wantErr, err)
}
assert.Equal(t, tt.want, got)

View file

@ -3,7 +3,6 @@ package ptp
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
@ -11,6 +10,7 @@ import (
"time"
"github.com/autobrr/autobrr/internal/domain"
"github.com/autobrr/autobrr/pkg/errors"
"golang.org/x/time/rate"
)
@ -87,11 +87,11 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
ctx := context.Background()
err := c.Ratelimiter.Wait(ctx) // This is a blocking call. Honors the rate limit
if err != nil {
return nil, err
return nil, errors.Wrap(err, "error waiting for ratelimiter")
}
resp, err := c.client.Do(req)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "error making request")
}
return resp, nil
}
@ -99,7 +99,7 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
func (c *Client) get(url string) (*http.Response, error) {
req, err := http.NewRequest(http.MethodGet, url, http.NoBody)
if err != nil {
return nil, fmt.Errorf("ptp client request error : %v", url)
return nil, errors.Wrap(err, "ptp client request error : %v", url)
}
req.Header.Add("ApiUser", c.APIUser)
@ -108,7 +108,7 @@ func (c *Client) get(url string) (*http.Response, error) {
res, err := c.Do(req)
if err != nil {
return nil, fmt.Errorf("ptp client request error : %v", url)
return nil, errors.Wrap(err, "ptp client request error : %v", url)
}
if res.StatusCode == http.StatusUnauthorized {
@ -124,7 +124,7 @@ func (c *Client) get(url string) (*http.Response, error) {
func (c *Client) GetTorrentByID(torrentID string) (*domain.TorrentBasic, error) {
if torrentID == "" {
return nil, fmt.Errorf("ptp client: must have torrentID")
return nil, errors.New("ptp client: must have torrentID")
}
var r TorrentResponse
@ -133,23 +133,23 @@ func (c *Client) GetTorrentByID(torrentID string) (*domain.TorrentBasic, error)
v.Add("torrentid", torrentID)
params := v.Encode()
url := fmt.Sprintf("%v?%v", c.Url, params)
reqUrl := fmt.Sprintf("%v?%v", c.Url, params)
resp, err := c.get(url)
resp, err := c.get(reqUrl)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "error requesting data")
}
defer resp.Body.Close()
body, readErr := ioutil.ReadAll(resp.Body)
if readErr != nil {
return nil, readErr
return nil, errors.Wrap(readErr, "could not read body")
}
err = json.Unmarshal(body, &r)
if err != nil {
return nil, err
return nil, errors.Wrap(readErr, "could not unmarshal body")
}
for _, torrent := range r.Torrents {
@ -169,7 +169,7 @@ func (c *Client) GetTorrentByID(torrentID string) (*domain.TorrentBasic, error)
func (c *Client) TestAPI() (bool, error) {
resp, err := c.get(c.Url)
if err != nil {
return false, err
return false, errors.Wrap(err, "error requesting data")
}
defer resp.Body.Close()

View file

@ -5,6 +5,7 @@ import (
"crypto/tls"
"fmt"
"io"
"log"
"mime/multipart"
"net/http"
"net/http/cookiejar"
@ -14,8 +15,9 @@ import (
"strings"
"time"
"github.com/rs/zerolog/log"
"golang.org/x/net/publicsuffix"
"github.com/autobrr/autobrr/pkg/errors"
publicsuffix "golang.org/x/net/publicsuffix"
)
var (
@ -31,6 +33,8 @@ type Client struct {
Name string
settings Settings
http *http.Client
Log *log.Logger
}
type Settings struct {
@ -43,6 +47,7 @@ type Settings struct {
protocol string
BasicAuth bool
Basic Basic
Log *log.Logger
}
type Basic struct {
@ -51,20 +56,24 @@ type Basic struct {
}
func NewClient(s Settings) *Client {
jarOptions := &cookiejar.Options{PublicSuffixList: publicsuffix.List}
//store cookies in jar
jar, err := cookiejar.New(jarOptions)
if err != nil {
log.Error().Err(err).Msg("new client cookie error")
}
httpClient := &http.Client{
Timeout: timeout,
Jar: jar,
}
c := &Client{
settings: s,
http: httpClient,
}
if s.Log == nil {
c.Log = log.New(io.Discard, "qbittorrent", log.LstdFlags)
}
//store cookies in jar
jarOptions := &cookiejar.Options{PublicSuffixList: publicsuffix.List}
jar, err := cookiejar.New(jarOptions)
if err != nil {
c.Log.Println("new client cookie error")
}
c.http = &http.Client{
Timeout: timeout,
Jar: jar,
}
c.settings.protocol = "http"
@ -92,8 +101,7 @@ func (c *Client) get(endpoint string, opts map[string]string) (*http.Response, e
req, err := http.NewRequest("GET", reqUrl, nil)
if err != nil {
log.Error().Err(err).Msgf("GET: error %v", reqUrl)
return nil, err
return nil, errors.Wrap(err, "could not build request")
}
if c.settings.BasicAuth {
@ -109,14 +117,13 @@ func (c *Client) get(endpoint string, opts map[string]string) (*http.Response, e
break
}
log.Debug().Msgf("qbit GET failed: retrying attempt %d - %v", i, reqUrl)
c.Log.Printf("qbit GET failed: retrying attempt %d - %v\n", i, reqUrl)
time.Sleep(backoff)
}
if err != nil {
log.Error().Err(err).Msgf("GET: do %v", reqUrl)
return nil, err
return nil, errors.Wrap(err, "error making get request: %v", reqUrl)
}
return resp, nil
@ -138,8 +145,7 @@ func (c *Client) post(endpoint string, opts map[string]string) (*http.Response,
req, err := http.NewRequest("POST", reqUrl, strings.NewReader(form.Encode()))
if err != nil {
log.Error().Err(err).Msgf("POST: req %v", reqUrl)
return nil, err
return nil, errors.Wrap(err, "could not build request")
}
if c.settings.BasicAuth {
@ -158,14 +164,13 @@ func (c *Client) post(endpoint string, opts map[string]string) (*http.Response,
break
}
log.Debug().Msgf("qbit POST failed: retrying attempt %d - %v", i, reqUrl)
c.Log.Printf("qbit POST failed: retrying attempt %d - %v\n", i, reqUrl)
time.Sleep(backoff)
}
if err != nil {
log.Error().Err(err).Msgf("POST: do %v", reqUrl)
return nil, err
return nil, errors.Wrap(err, "error making post request: %v", reqUrl)
}
return resp, nil
@ -187,8 +192,7 @@ func (c *Client) postBasic(endpoint string, opts map[string]string) (*http.Respo
req, err := http.NewRequest("POST", reqUrl, strings.NewReader(form.Encode()))
if err != nil {
log.Error().Err(err).Msgf("POST: req %v", reqUrl)
return nil, err
return nil, errors.Wrap(err, "could not build request")
}
if c.settings.BasicAuth {
@ -200,8 +204,7 @@ func (c *Client) postBasic(endpoint string, opts map[string]string) (*http.Respo
resp, err = c.http.Do(req)
if err != nil {
log.Error().Err(err).Msgf("POST: do %v", reqUrl)
return nil, err
return nil, errors.Wrap(err, "error making post request: %v", reqUrl)
}
return resp, nil
@ -213,8 +216,7 @@ func (c *Client) postFile(endpoint string, fileName string, opts map[string]stri
file, err := os.Open(fileName)
if err != nil {
log.Error().Err(err).Msgf("POST file: opening file %v", fileName)
return nil, err
return nil, errors.Wrap(err, "error opening file %v", fileName)
}
// Close the file later
defer file.Close()
@ -228,15 +230,13 @@ func (c *Client) postFile(endpoint string, fileName string, opts map[string]stri
// Initialize file field
fileWriter, err := multiPartWriter.CreateFormFile("torrents", fileName)
if err != nil {
log.Error().Err(err).Msgf("POST file: initializing file field %v", fileName)
return nil, err
return nil, errors.Wrap(err, "error initializing file field %v", fileName)
}
// Copy the actual file content to the fields writer
_, err = io.Copy(fileWriter, file)
if err != nil {
log.Error().Err(err).Msgf("POST file: could not copy file to writer %v", fileName)
return nil, err
return nil, errors.Wrap(err, "error copy file contents to writer %v", fileName)
}
// Populate other fields
@ -244,14 +244,12 @@ func (c *Client) postFile(endpoint string, fileName string, opts map[string]stri
for key, val := range opts {
fieldWriter, err := multiPartWriter.CreateFormField(key)
if err != nil {
log.Error().Err(err).Msgf("POST file: could not add other fields %v", fileName)
return nil, err
return nil, errors.Wrap(err, "error creating form field %v with value %v", key, val)
}
_, err = fieldWriter.Write([]byte(val))
if err != nil {
log.Error().Err(err).Msgf("POST file: could not write field %v", fileName)
return nil, err
return nil, errors.Wrap(err, "error writing field %v with value %v", key, val)
}
}
}
@ -262,8 +260,7 @@ func (c *Client) postFile(endpoint string, fileName string, opts map[string]stri
reqUrl := buildUrl(c.settings, endpoint)
req, err := http.NewRequest("POST", reqUrl, &requestBody)
if err != nil {
log.Error().Err(err).Msgf("POST file: could not create request object %v", fileName)
return nil, err
return nil, errors.Wrap(err, "error creating request %v", fileName)
}
if c.settings.BasicAuth {
@ -282,14 +279,13 @@ func (c *Client) postFile(endpoint string, fileName string, opts map[string]stri
break
}
log.Debug().Msgf("qbit POST file failed: retrying attempt %d - %v", i, reqUrl)
c.Log.Printf("qbit POST file failed: retrying attempt %d - %v\n", i, reqUrl)
time.Sleep(backoff)
}
if err != nil {
log.Error().Err(err).Msgf("POST file: could not perform request %v", fileName)
return nil, err
return nil, errors.Wrap(err, "error making post file request %v", fileName)
}
return resp, nil

View file

@ -2,14 +2,13 @@ package qbittorrent
import (
"encoding/json"
"errors"
"io/ioutil"
"net/http"
"net/http/httputil"
"strconv"
"strings"
"github.com/rs/zerolog/log"
"github.com/autobrr/autobrr/pkg/errors"
)
// Login https://github.com/qbittorrent/qBittorrent/wiki/WebUI-API-(qBittorrent-4.1)#authentication
@ -21,15 +20,12 @@ func (c *Client) Login() error {
resp, err := c.postBasic("auth/login", opts)
if err != nil {
log.Error().Err(err).Msg("login error")
return err
return errors.Wrap(err, "login error")
} else if resp.StatusCode == http.StatusForbidden {
log.Error().Err(err).Msg("User's IP is banned for too many failed login attempts")
return err
return errors.New("User's IP is banned for too many failed login attempts")
} else if resp.StatusCode != http.StatusOK { // check for correct status code
log.Error().Err(err).Msgf("login bad status %v error", resp.StatusCode)
return errors.New("qbittorrent login bad status")
return errors.New("qbittorrent login bad status %v", resp.StatusCode)
}
defer resp.Body.Close()
@ -61,23 +57,20 @@ func (c *Client) GetTorrents() ([]Torrent, error) {
resp, err := c.get("torrents/info", nil)
if err != nil {
log.Error().Err(err).Msg("get torrents error")
return nil, err
return nil, errors.Wrap(err, "get torrents error")
}
defer resp.Body.Close()
body, readErr := ioutil.ReadAll(resp.Body)
if readErr != nil {
log.Error().Err(err).Msg("get torrents read error")
return nil, readErr
return nil, errors.Wrap(readErr, "could not read body")
}
var torrents []Torrent
err = json.Unmarshal(body, &torrents)
if err != nil {
log.Error().Err(err).Msg("get torrents unmarshal error")
return nil, err
return nil, errors.Wrap(err, "could not unmarshal body")
}
return torrents, nil
@ -90,23 +83,20 @@ func (c *Client) GetTorrentsFilter(filter TorrentFilter) ([]Torrent, error) {
resp, err := c.get("torrents/info", opts)
if err != nil {
log.Error().Err(err).Msgf("get filtered torrents error: %v", filter)
return nil, err
return nil, errors.Wrap(err, "could not get filtered torrents with filter: %v", filter)
}
defer resp.Body.Close()
body, readErr := ioutil.ReadAll(resp.Body)
if readErr != nil {
log.Error().Err(err).Msgf("get filtered torrents read error: %v", filter)
return nil, readErr
return nil, errors.Wrap(readErr, "could not read body")
}
var torrents []Torrent
err = json.Unmarshal(body, &torrents)
if err != nil {
log.Error().Err(err).Msgf("get filtered torrents unmarshal error: %v", filter)
return nil, err
return nil, errors.Wrap(err, "could not unmarshal body")
}
return torrents, nil
@ -121,23 +111,20 @@ func (c *Client) GetTorrentsActiveDownloads() ([]Torrent, error) {
resp, err := c.get("torrents/info", opts)
if err != nil {
log.Error().Err(err).Msgf("get filtered torrents error: %v", filter)
return nil, err
return nil, errors.Wrap(err, "could not get active torrents")
}
defer resp.Body.Close()
body, readErr := ioutil.ReadAll(resp.Body)
if readErr != nil {
log.Error().Err(err).Msgf("get filtered torrents read error: %v", filter)
return nil, readErr
return nil, errors.Wrap(readErr, "could not read body")
}
var torrents []Torrent
err = json.Unmarshal(body, &torrents)
if err != nil {
log.Error().Err(err).Msgf("get filtered torrents unmarshal error: %v", filter)
return nil, err
return nil, errors.Wrap(readErr, "could not unmarshal body")
}
res := make([]Torrent, 0)
@ -155,13 +142,15 @@ func (c *Client) GetTorrentsActiveDownloads() ([]Torrent, error) {
func (c *Client) GetTorrentsRaw() (string, error) {
resp, err := c.get("torrents/info", nil)
if err != nil {
log.Error().Err(err).Msg("get torrent trackers raw error")
return "", err
return "", errors.Wrap(err, "could not get torrents raw")
}
defer resp.Body.Close()
data, _ := ioutil.ReadAll(resp.Body)
data, err := ioutil.ReadAll(resp.Body)
if err != nil {
return "", errors.Wrap(err, "could not get read body torrents raw")
}
return string(data), nil
}
@ -173,40 +162,35 @@ func (c *Client) GetTorrentTrackers(hash string) ([]TorrentTracker, error) {
resp, err := c.get("torrents/trackers", opts)
if err != nil {
log.Error().Err(err).Msgf("get torrent trackers error: %v", hash)
return nil, err
return nil, errors.Wrap(err, "could not get torrent trackers for hash: %v", hash)
}
defer resp.Body.Close()
dump, err := httputil.DumpResponse(resp, true)
if err != nil {
log.Error().Err(err).Msgf("get torrent trackers dump response error: %v", err)
c.Log.Printf("get torrent trackers error dump response: %v\n", string(dump))
}
log.Trace().Msgf("get torrent trackers response dump: %v", string(dump))
c.Log.Printf("get torrent trackers response dump: %v\n", string(dump))
if resp.StatusCode == http.StatusNotFound {
//return nil, fmt.Errorf("torrent not found: %v", hash)
return nil, nil
} else if resp.StatusCode == http.StatusForbidden {
//return nil, fmt.Errorf("torrent not found: %v", hash)
return nil, nil
}
body, readErr := ioutil.ReadAll(resp.Body)
if readErr != nil {
log.Error().Err(err).Msgf("get torrent trackers read error: %v", hash)
return nil, readErr
return nil, errors.Wrap(err, "could not read body")
}
log.Trace().Msgf("get torrent trackers body: %v", string(body))
c.Log.Printf("get torrent trackers body: %v\n", string(body))
var trackers []TorrentTracker
err = json.Unmarshal(body, &trackers)
if err != nil {
log.Error().Err(err).Msgf("get torrent trackers: %v", hash)
return nil, err
return nil, errors.Wrap(err, "could not unmarshal body")
}
return trackers, nil
@ -217,11 +201,9 @@ func (c *Client) AddTorrentFromFile(file string, options map[string]string) erro
res, err := c.postFile("torrents/add", file, options)
if err != nil {
log.Error().Err(err).Msgf("add torrents error: %v", file)
return err
return errors.Wrap(err, "could not add torrent %v", file)
} else if res.StatusCode != http.StatusOK {
log.Error().Err(err).Msgf("add torrents bad status: %v", file)
return err
return errors.Wrap(err, "could not add torrent %v unexpected status: %v", file, res.StatusCode)
}
defer res.Body.Close()
@ -240,11 +222,9 @@ func (c *Client) DeleteTorrents(hashes []string, deleteFiles bool) error {
resp, err := c.get("torrents/delete", opts)
if err != nil {
log.Error().Err(err).Msgf("delete torrents error: %v", hashes)
return err
return errors.Wrap(err, "could not delete torrents: %+v", hashes)
} else if resp.StatusCode != http.StatusOK {
log.Error().Err(err).Msgf("delete torrents bad code: %v", hashes)
return err
return errors.Wrap(err, "could not delete torrents %v unexpected status: %v", hashes, resp.StatusCode)
}
defer resp.Body.Close()
@ -261,11 +241,9 @@ func (c *Client) ReAnnounceTorrents(hashes []string) error {
resp, err := c.get("torrents/reannounce", opts)
if err != nil {
log.Error().Err(err).Msgf("re-announce error: %v", hashes)
return err
return errors.Wrap(err, "could not re-announce torrents: %v", hashes)
} else if resp.StatusCode != http.StatusOK {
log.Error().Err(err).Msgf("re-announce error bad status: %v", hashes)
return err
return errors.Wrap(err, "could not re-announce torrents: %v unexpected status: %v", hashes, resp.StatusCode)
}
defer resp.Body.Close()
@ -276,23 +254,20 @@ func (c *Client) ReAnnounceTorrents(hashes []string) error {
func (c *Client) GetTransferInfo() (*TransferInfo, error) {
resp, err := c.get("transfer/info", nil)
if err != nil {
log.Error().Err(err).Msg("get torrents error")
return nil, err
return nil, errors.Wrap(err, "could not get transfer info")
}
defer resp.Body.Close()
body, readErr := ioutil.ReadAll(resp.Body)
if readErr != nil {
log.Error().Err(err).Msg("get torrents read error")
return nil, readErr
return nil, errors.Wrap(readErr, "could not read body")
}
var info TransferInfo
err = json.Unmarshal(body, &info)
if err != nil {
log.Error().Err(err).Msg("get torrents unmarshal error")
return nil, err
return nil, errors.Wrap(readErr, "could not unmarshal body")
}
return &info, nil

View file

@ -3,14 +3,12 @@ package radarr
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path"
"github.com/rs/zerolog/log"
"github.com/autobrr/autobrr/pkg/errors"
)
func (c *client) get(endpoint string) (int, []byte, error) {
@ -20,8 +18,7 @@ func (c *client) get(endpoint string) (int, []byte, error) {
req, err := http.NewRequest(http.MethodGet, reqUrl, http.NoBody)
if err != nil {
log.Error().Err(err).Msgf("radarr client request error : %v", reqUrl)
return 0, nil, err
return 0, nil, errors.Wrap(err, "could not build request: %v", reqUrl)
}
if c.config.BasicAuth {
@ -32,15 +29,14 @@ func (c *client) get(endpoint string) (int, []byte, error) {
resp, err := c.http.Do(req)
if err != nil {
log.Error().Err(err).Msgf("radarr client.get request error: %v", reqUrl)
return 0, nil, fmt.Errorf("radarr.http.Do(req): %w", err)
return 0, nil, errors.Wrap(err, "radarr.http.Do(req): %v", reqUrl)
}
defer resp.Body.Close()
var buf bytes.Buffer
if _, err = io.Copy(&buf, resp.Body); err != nil {
return resp.StatusCode, nil, fmt.Errorf("radarr.io.Copy: %w", err)
return resp.StatusCode, nil, errors.Wrap(err, "radarr.io.Copy")
}
return resp.StatusCode, buf.Bytes(), nil
@ -53,14 +49,12 @@ func (c *client) post(endpoint string, data interface{}) (*http.Response, error)
jsonData, err := json.Marshal(data)
if err != nil {
log.Error().Err(err).Msgf("radarr client could not marshal data: %v", reqUrl)
return nil, err
return nil, errors.Wrap(err, "could not marshal data: %+v", data)
}
req, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(jsonData))
if err != nil {
log.Error().Err(err).Msgf("radarr client request error: %v", reqUrl)
return nil, err
return nil, errors.Wrap(err, "could not build request: %v", reqUrl)
}
if c.config.BasicAuth {
@ -73,19 +67,15 @@ func (c *client) post(endpoint string, data interface{}) (*http.Response, error)
res, err := c.http.Do(req)
if err != nil {
log.Error().Err(err).Msgf("radarr client request error: %v", reqUrl)
return nil, err
return nil, errors.Wrap(err, "could not make request: %+v", req)
}
// validate response
if res.StatusCode == http.StatusUnauthorized {
log.Error().Err(err).Msgf("radarr client bad request: %v", reqUrl)
return nil, errors.New("unauthorized: bad credentials")
} else if res.StatusCode == http.StatusBadRequest {
log.Error().Err(err).Msgf("radarr client request error: %v", reqUrl)
return nil, errors.New("radarr: bad request")
} else if res.StatusCode != http.StatusOK {
log.Error().Err(err).Msgf("radarr client request error: %v", reqUrl)
return nil, errors.New("radarr: bad request")
}
@ -100,14 +90,12 @@ func (c *client) postBody(endpoint string, data interface{}) (int, []byte, error
jsonData, err := json.Marshal(data)
if err != nil {
log.Error().Err(err).Msgf("radarr client could not marshal data: %v", reqUrl)
return 0, nil, err
return 0, nil, errors.Wrap(err, "could not marshal data: %+v", data)
}
req, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(jsonData))
if err != nil {
log.Error().Err(err).Msgf("radarr client request error: %v", reqUrl)
return 0, nil, err
return 0, nil, errors.Wrap(err, "could not build request: %v", reqUrl)
}
if c.config.BasicAuth {
@ -118,19 +106,20 @@ func (c *client) postBody(endpoint string, data interface{}) (int, []byte, error
resp, err := c.http.Do(req)
if err != nil {
log.Error().Err(err).Msgf("radarr client request error: %v", reqUrl)
return 0, nil, fmt.Errorf("radarr.http.Do(req): %w", err)
return 0, nil, errors.Wrap(err, "radarr.http.Do(req): %+v", req)
}
defer resp.Body.Close()
var buf bytes.Buffer
if _, err = io.Copy(&buf, resp.Body); err != nil {
return resp.StatusCode, nil, fmt.Errorf("radarr.io.Copy: %w", err)
return resp.StatusCode, nil, errors.Wrap(err, "radarr.io.Copy")
}
if resp.StatusCode < 200 || resp.StatusCode > 299 {
return resp.StatusCode, buf.Bytes(), fmt.Errorf("radarr: bad request: %v (status: %s): %s", resp.Request.RequestURI, resp.Status, buf.String())
if resp.StatusCode == http.StatusBadRequest {
return resp.StatusCode, buf.Bytes(), nil
} else if resp.StatusCode < 200 || resp.StatusCode > 401 {
return resp.StatusCode, buf.Bytes(), errors.New("radarr: bad request: %v (status: %s): %s", resp.Request.RequestURI, resp.Status, buf.String())
}
return resp.StatusCode, buf.Bytes(), nil

View file

@ -2,12 +2,14 @@ package radarr
import (
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
"github.com/rs/zerolog/log"
"github.com/autobrr/autobrr/pkg/errors"
)
type Config struct {
@ -18,6 +20,8 @@ type Config struct {
BasicAuth bool
Username string
Password string
Log *log.Logger
}
type Client interface {
@ -28,6 +32,8 @@ type Client interface {
type client struct {
config Config
http *http.Client
Log *log.Logger
}
func New(config Config) Client {
@ -39,6 +45,11 @@ func New(config Config) Client {
c := &client{
config: config,
http: httpClient,
Log: config.Log,
}
if config.Log == nil {
c.Log = log.New(io.Discard, "", log.LstdFlags)
}
return c
@ -65,11 +76,17 @@ type SystemStatusResponse struct {
Version string `json:"version"`
}
type BadRequestResponse struct {
PropertyName string `json:"propertyName"`
ErrorMessage string `json:"errorMessage"`
AttemptedValue string `json:"attemptedValue"`
Severity string `json:"severity"`
}
func (c *client) Test() (*SystemStatusResponse, error) {
status, res, err := c.get("system/status")
if err != nil {
log.Error().Stack().Err(err).Msg("radarr client get error")
return nil, err
return nil, errors.Wrap(err, "radarr error running test")
}
if status == http.StatusUnauthorized {
@ -79,11 +96,10 @@ func (c *client) Test() (*SystemStatusResponse, error) {
response := SystemStatusResponse{}
err = json.Unmarshal(res, &response)
if err != nil {
log.Error().Stack().Err(err).Msg("radarr client error json unmarshal")
return nil, err
return nil, errors.Wrap(err, "could not unmarshal data")
}
log.Trace().Msgf("radarr system/status response: %+v", response)
c.Log.Printf("radarr system/status status: (%v) response: %v\n", status, string(res))
return &response, nil
}
@ -91,24 +107,35 @@ func (c *client) Test() (*SystemStatusResponse, error) {
func (c *client) Push(release Release) ([]string, error) {
status, res, err := c.postBody("release/push", release)
if err != nil {
log.Error().Stack().Err(err).Msgf("radarr client post error. status: %d", status)
return nil, err
return nil, errors.Wrap(err, "error push release")
}
c.Log.Printf("radarr release/push status: (%v) response: %v\n", status, string(res))
if status == http.StatusBadRequest {
badreqResponse := make([]*BadRequestResponse, 0)
err = json.Unmarshal(res, &badreqResponse)
if err != nil {
return nil, errors.Wrap(err, "could not unmarshal data")
}
if badreqResponse[0] != nil && badreqResponse[0].PropertyName == "Title" && badreqResponse[0].ErrorMessage == "Unable to parse" {
rejections := []string{fmt.Sprintf("unable to parse: %v", badreqResponse[0].AttemptedValue)}
return rejections, nil
}
}
pushResponse := make([]PushResponse, 0)
err = json.Unmarshal(res, &pushResponse)
if err != nil {
log.Error().Stack().Err(err).Msg("radarr client error json unmarshal")
return nil, err
return nil, errors.Wrap(err, "could not unmarshal data")
}
log.Trace().Msgf("radarr release/push response status: %v body: %+v", status, string(res))
// log and return if rejected
if pushResponse[0].Rejected {
rejections := strings.Join(pushResponse[0].Rejections, ", ")
log.Trace().Msgf("radarr push rejected: %s - reasons: %q", release.Title, rejections)
c.Log.Printf("radarr release/push rejected %v reasons: %q\n", release.Title, rejections)
return pushResponse[0].Rejections, nil
}

View file

@ -1,7 +1,6 @@
package radarr
import (
"errors"
"io/ioutil"
"net/http"
"net/http/httptest"
@ -90,8 +89,6 @@ func Test_client_Push(t *testing.T) {
PublishDate: "2021-08-21T15:36:00Z",
}},
rejections: []string{"Could not find Some Old Movie"},
//err: errors.New("radarr push rejected Could not find Some Old Movie"),
//wantErr: true,
},
{
name: "push_error",
@ -114,8 +111,6 @@ func Test_client_Push(t *testing.T) {
PublishDate: "2021-08-21T15:36:00Z",
}},
rejections: []string{"Could not find Some Old Movie"},
//err: errors.New("radarr push rejected Could not find Some Old Movie"),
//wantErr: true,
},
{
name: "push_parse_error",
@ -137,8 +132,8 @@ func Test_client_Push(t *testing.T) {
Protocol: "torrent",
PublishDate: "2021-08-21T15:36:00Z",
}},
err: errors.New("radarr: bad request: (status: 400 Bad Request): [\n {\n \"propertyName\": \"Title\",\n \"errorMessage\": \"Unable to parse\",\n \"attemptedValue\": \"Minx 1 epi 9 2160p\",\n \"severity\": \"error\"\n }\n]\n"),
wantErr: true,
rejections: []string{"unable to parse: Minx 1 epi 9 2160p"},
wantErr: false,
},
}
for _, tt := range tests {
@ -177,11 +172,11 @@ func Test_client_Test(t *testing.T) {
defer srv.Close()
tests := []struct {
name string
cfg Config
want *SystemStatusResponse
err error
wantErr bool
name string
cfg Config
want *SystemStatusResponse
expectedErr string
wantErr bool
}{
{
name: "fetch",
@ -192,9 +187,9 @@ func Test_client_Test(t *testing.T) {
Username: "",
Password: "",
},
want: &SystemStatusResponse{Version: "3.2.2.5080"},
err: nil,
wantErr: false,
want: &SystemStatusResponse{Version: "3.2.2.5080"},
expectedErr: "",
wantErr: false,
},
{
name: "fetch_unauthorized",
@ -205,9 +200,9 @@ func Test_client_Test(t *testing.T) {
Username: "",
Password: "",
},
want: nil,
wantErr: true,
err: errors.New("unauthorized: bad credentials"),
want: nil,
wantErr: true,
expectedErr: "unauthorized: bad credentials",
},
{
name: "fetch_subfolder",
@ -218,9 +213,9 @@ func Test_client_Test(t *testing.T) {
Username: "",
Password: "",
},
want: &SystemStatusResponse{Version: "3.2.2.5080"},
err: nil,
wantErr: false,
want: &SystemStatusResponse{Version: "3.2.2.5080"},
expectedErr: "",
wantErr: false,
},
}
for _, tt := range tests {
@ -229,7 +224,7 @@ func Test_client_Test(t *testing.T) {
got, err := c.Test()
if tt.wantErr && assert.Error(t, err) {
assert.Equal(t, tt.err, err)
assert.EqualErrorf(t, err, tt.expectedErr, "Error should be: %v, got: %v", tt.wantErr, err)
}
assert.Equal(t, tt.want, got)

View file

@ -3,18 +3,17 @@ package red
import (
"context"
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"strconv"
"time"
"github.com/rs/zerolog/log"
"golang.org/x/time/rate"
"github.com/autobrr/autobrr/internal/domain"
"github.com/autobrr/autobrr/pkg/errors"
"golang.org/x/time/rate"
)
type REDClient interface {
@ -131,8 +130,7 @@ func (c *Client) Do(req *http.Request) (*http.Response, error) {
func (c *Client) get(url string) (*http.Response, error) {
req, err := http.NewRequest(http.MethodGet, url, http.NoBody)
if err != nil {
log.Error().Err(err).Msgf("red client request error : %v", url)
return nil, err
return nil, errors.Wrap(err, "could not build request")
}
req.Header.Add("Authorization", c.APIKey)
@ -140,8 +138,7 @@ func (c *Client) get(url string) (*http.Response, error) {
res, err := c.Do(req)
if err != nil {
log.Error().Err(err).Msgf("red client request error : %v", url)
return nil, err
return nil, errors.Wrap(err, "could not make request: %+v", req)
}
if res.StatusCode == http.StatusUnauthorized {
@ -159,7 +156,7 @@ func (c *Client) get(url string) (*http.Response, error) {
func (c *Client) GetTorrentByID(torrentID string) (*domain.TorrentBasic, error) {
if torrentID == "" {
return nil, fmt.Errorf("red client: must have torrentID")
return nil, errors.New("red client: must have torrentID")
}
var r TorrentDetailsResponse
@ -168,23 +165,23 @@ func (c *Client) GetTorrentByID(torrentID string) (*domain.TorrentBasic, error)
v.Add("id", torrentID)
params := v.Encode()
url := fmt.Sprintf("%v?action=torrent&%v", c.URL, params)
reqUrl := fmt.Sprintf("%v?action=torrent&%v", c.URL, params)
resp, err := c.get(url)
resp, err := c.get(reqUrl)
if err != nil {
return nil, err
return nil, errors.Wrap(err, "could not get torrent by id: %v", torrentID)
}
defer resp.Body.Close()
body, readErr := ioutil.ReadAll(resp.Body)
if readErr != nil {
return nil, readErr
return nil, errors.Wrap(readErr, "could not read body")
}
err = json.Unmarshal(body, &r)
if err != nil {
return nil, err
return nil, errors.Wrap(readErr, "could not unmarshal body")
}
return &domain.TorrentBasic{
@ -199,7 +196,7 @@ func (c *Client) GetTorrentByID(torrentID string) (*domain.TorrentBasic, error)
func (c *Client) TestAPI() (bool, error) {
resp, err := c.get(c.URL + "?action=index")
if err != nil {
return false, err
return false, errors.Wrap(err, "could not run test api")
}
defer resp.Body.Close()

View file

@ -1,17 +1,15 @@
package red
import (
"errors"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/autobrr/autobrr/internal/domain"
"github.com/rs/zerolog"
"github.com/stretchr/testify/assert"
"github.com/autobrr/autobrr/internal/domain"
)
func TestREDClient_GetTorrentByID(t *testing.T) {
@ -57,7 +55,7 @@ func TestREDClient_GetTorrentByID(t *testing.T) {
fields fields
args args
want *domain.TorrentBasic
wantErr error
wantErr string
}{
{
name: "get_by_id_1",
@ -71,7 +69,7 @@ func TestREDClient_GetTorrentByID(t *testing.T) {
InfoHash: "B2BABD3A361EAFC6C4E9142C422DF7DDF5D7E163",
Size: "527749302",
},
wantErr: nil,
wantErr: "",
},
{
name: "get_by_id_2",
@ -81,7 +79,7 @@ func TestREDClient_GetTorrentByID(t *testing.T) {
},
args: args{torrentID: "100002"},
want: nil,
wantErr: errors.New("bad id parameter"),
wantErr: "could not get torrent by id: 100002: bad id parameter",
},
{
name: "get_by_id_3",
@ -91,7 +89,7 @@ func TestREDClient_GetTorrentByID(t *testing.T) {
},
args: args{torrentID: "100002"},
want: nil,
wantErr: errors.New("unauthorized: bad credentials"),
wantErr: "could not get torrent by id: 100002: unauthorized: bad credentials",
},
}
for _, tt := range tests {
@ -99,8 +97,8 @@ func TestREDClient_GetTorrentByID(t *testing.T) {
c := NewClient(tt.fields.Url, tt.fields.APIKey)
got, err := c.GetTorrentByID(tt.args.torrentID)
if tt.wantErr != nil && assert.Error(t, err) {
assert.Equal(t, tt.wantErr, err)
if tt.wantErr != "" && assert.Error(t, err) {
assert.EqualErrorf(t, err, tt.wantErr, "Error should be: %v, got: %v", tt.wantErr, err)
}
assert.Equal(t, tt.want, got)

View file

@ -3,14 +3,12 @@ package sonarr
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"path"
"github.com/rs/zerolog/log"
"github.com/autobrr/autobrr/pkg/errors"
)
func (c *client) get(endpoint string) (int, []byte, error) {
@ -20,8 +18,7 @@ func (c *client) get(endpoint string) (int, []byte, error) {
req, err := http.NewRequest(http.MethodGet, reqUrl, http.NoBody)
if err != nil {
log.Error().Err(err).Msgf("sonarr client request error : %v", reqUrl)
return 0, nil, err
return 0, nil, errors.Wrap(err, "could not build request")
}
if c.config.BasicAuth {
@ -32,15 +29,14 @@ func (c *client) get(endpoint string) (int, []byte, error) {
resp, err := c.http.Do(req)
if err != nil {
log.Error().Err(err).Msgf("sonarr client.get request error: %v", reqUrl)
return 0, nil, fmt.Errorf("sonarr.http.Do(req): %w", err)
return 0, nil, errors.Wrap(err, "sonarr.http.Do(req): %+v", req)
}
defer resp.Body.Close()
var buf bytes.Buffer
if _, err = io.Copy(&buf, resp.Body); err != nil {
return resp.StatusCode, nil, fmt.Errorf("sonarr.io.Copy: %w", err)
return resp.StatusCode, nil, errors.Wrap(err, "sonarr.io.Copy")
}
return resp.StatusCode, buf.Bytes(), nil
@ -53,14 +49,12 @@ func (c *client) post(endpoint string, data interface{}) (*http.Response, error)
jsonData, err := json.Marshal(data)
if err != nil {
log.Error().Err(err).Msgf("sonarr client could not marshal data: %v", reqUrl)
return nil, err
return nil, errors.Wrap(err, "could not marshal data: %+v", data)
}
req, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(jsonData))
if err != nil {
log.Error().Err(err).Msgf("sonarr client request error: %v", reqUrl)
return nil, err
return nil, errors.Wrap(err, "could not build request")
}
if c.config.BasicAuth {
@ -73,16 +67,13 @@ func (c *client) post(endpoint string, data interface{}) (*http.Response, error)
res, err := c.http.Do(req)
if err != nil {
log.Error().Err(err).Msgf("sonarr client request error: %v", reqUrl)
return nil, err
return nil, errors.Wrap(err, "could not make request: %+v", req)
}
// validate response
if res.StatusCode == http.StatusUnauthorized {
log.Error().Err(err).Msgf("sonarr client bad request: %v", reqUrl)
return nil, errors.New("unauthorized: bad credentials")
} else if res.StatusCode != http.StatusOK {
log.Error().Err(err).Msgf("sonarr client request error: %v", reqUrl)
return nil, errors.New("sonarr: bad request")
}
@ -97,14 +88,12 @@ func (c *client) postBody(endpoint string, data interface{}) (int, []byte, error
jsonData, err := json.Marshal(data)
if err != nil {
log.Error().Err(err).Msgf("sonarr client could not marshal data: %v", reqUrl)
return 0, nil, err
return 0, nil, errors.Wrap(err, "could not marshal data: %+v", data)
}
req, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(jsonData))
if err != nil {
log.Error().Err(err).Msgf("sonarr client request error: %v", reqUrl)
return 0, nil, err
return 0, nil, errors.Wrap(err, "could not build request")
}
if c.config.BasicAuth {
@ -115,19 +104,20 @@ func (c *client) postBody(endpoint string, data interface{}) (int, []byte, error
resp, err := c.http.Do(req)
if err != nil {
log.Error().Err(err).Msgf("sonarr client request error: %v", reqUrl)
return 0, nil, fmt.Errorf("sonarr.http.Do(req): %w", err)
return 0, nil, errors.Wrap(err, "sonarr.http.Do(req): %+v", req)
}
defer resp.Body.Close()
var buf bytes.Buffer
if _, err = io.Copy(&buf, resp.Body); err != nil {
return resp.StatusCode, nil, fmt.Errorf("sonarr.io.Copy: %w", err)
return resp.StatusCode, nil, errors.Wrap(err, "sonarr.io.Copy")
}
if resp.StatusCode < 200 || resp.StatusCode > 299 {
return resp.StatusCode, buf.Bytes(), fmt.Errorf("sonarr: bad request: %v (status: %s): %s", resp.Request.RequestURI, resp.Status, buf.String())
if resp.StatusCode == http.StatusBadRequest {
return resp.StatusCode, buf.Bytes(), nil
} else if resp.StatusCode < 200 || resp.StatusCode > 401 {
return resp.StatusCode, buf.Bytes(), errors.New("sonarr: bad request: %v (status: %s): %s", resp.Request.RequestURI, resp.Status, buf.String())
}
return resp.StatusCode, buf.Bytes(), nil

View file

@ -2,12 +2,15 @@ package sonarr
import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/rs/zerolog/log"
"log"
"github.com/autobrr/autobrr/pkg/errors"
)
type Config struct {
@ -18,6 +21,8 @@ type Config struct {
BasicAuth bool
Username string
Password string
Log *log.Logger
}
type Client interface {
@ -28,6 +33,8 @@ type Client interface {
type client struct {
config Config
http *http.Client
Log *log.Logger
}
// New create new sonarr client
@ -40,6 +47,12 @@ func New(config Config) Client {
c := &client{
config: config,
http: httpClient,
Log: config.Log,
}
if config.Log == nil {
// if no provided logger then use io.Discard
c.Log = log.New(io.Discard, "", log.LstdFlags)
}
return c
@ -62,6 +75,13 @@ type PushResponse struct {
Rejections []string `json:"rejections"`
}
type BadRequestResponse struct {
PropertyName string `json:"propertyName"`
ErrorMessage string `json:"errorMessage"`
AttemptedValue string `json:"attemptedValue"`
Severity string `json:"severity"`
}
type SystemStatusResponse struct {
Version string `json:"version"`
}
@ -69,21 +89,19 @@ type SystemStatusResponse struct {
func (c *client) Test() (*SystemStatusResponse, error) {
status, res, err := c.get("system/status")
if err != nil {
log.Error().Stack().Err(err).Msg("sonarr client get error")
return nil, err
return nil, errors.Wrap(err, "could not make Test")
}
if status == http.StatusUnauthorized {
return nil, errors.New("unauthorized: bad credentials")
}
log.Trace().Msgf("sonarr system/status response: %v", string(res))
c.Log.Printf("sonarr system/status status: (%v) response: %v\n", status, string(res))
response := SystemStatusResponse{}
err = json.Unmarshal(res, &response)
if err != nil {
log.Error().Stack().Err(err).Msg("sonarr client error json unmarshal")
return nil, err
return nil, errors.Wrap(err, "could not unmarshal data")
}
return &response, nil
@ -92,24 +110,35 @@ func (c *client) Test() (*SystemStatusResponse, error) {
func (c *client) Push(release Release) ([]string, error) {
status, res, err := c.postBody("release/push", release)
if err != nil {
log.Error().Stack().Err(err).Msg("sonarr client post error")
return nil, err
return nil, errors.Wrap(err, "could not push release to sonarr")
}
log.Trace().Msgf("sonarr release/push response status: (%v) body: %v", status, string(res))
c.Log.Printf("sonarr release/push status: (%v) response: %v\n", status, string(res))
if status == http.StatusBadRequest {
badreqResponse := make([]*BadRequestResponse, 0)
err = json.Unmarshal(res, &badreqResponse)
if err != nil {
return nil, errors.Wrap(err, "could not unmarshal data")
}
if badreqResponse[0] != nil && badreqResponse[0].PropertyName == "Title" && badreqResponse[0].ErrorMessage == "Unable to parse" {
rejections := []string{fmt.Sprintf("unable to parse: %v", badreqResponse[0].AttemptedValue)}
return rejections, err
}
}
pushResponse := make([]PushResponse, 0)
err = json.Unmarshal(res, &pushResponse)
if err != nil {
log.Error().Stack().Err(err).Msg("sonarr client error json unmarshal")
return nil, err
return nil, errors.Wrap(err, "could not unmarshal data")
}
// log and return if rejected
if pushResponse[0].Rejected {
rejections := strings.Join(pushResponse[0].Rejections, ", ")
log.Trace().Msgf("sonarr push rejected: %s - reasons: %q", release.Title, rejections)
c.Log.Printf("sonarr release/push rejected %v reasons: %q\n", release.Title, rejections)
return pushResponse[0].Rejections, nil
}

View file

@ -1,8 +1,8 @@
package sonarr
import (
"errors"
"io/ioutil"
"log"
"net/http"
"net/http/httptest"
"testing"
@ -15,6 +15,7 @@ import (
func Test_client_Push(t *testing.T) {
// disable logger
zerolog.SetGlobalLevel(zerolog.Disabled)
log.SetOutput(ioutil.Discard)
mux := http.NewServeMux()
ts := httptest.NewServer(mux)
@ -119,6 +120,7 @@ func Test_client_Push(t *testing.T) {
func Test_client_Test(t *testing.T) {
// disable logger
zerolog.SetGlobalLevel(zerolog.Disabled)
log.SetOutput(ioutil.Discard)
key := "mock-key"
@ -139,11 +141,11 @@ func Test_client_Test(t *testing.T) {
defer srv.Close()
tests := []struct {
name string
cfg Config
want *SystemStatusResponse
err error
wantErr bool
name string
cfg Config
want *SystemStatusResponse
expectedErr string
wantErr bool
}{
{
name: "fetch",
@ -154,9 +156,9 @@ func Test_client_Test(t *testing.T) {
Username: "",
Password: "",
},
want: &SystemStatusResponse{Version: "3.0.6.1196"},
err: nil,
wantErr: false,
want: &SystemStatusResponse{Version: "3.0.6.1196"},
expectedErr: "",
wantErr: false,
},
{
name: "fetch_unauthorized",
@ -167,9 +169,9 @@ func Test_client_Test(t *testing.T) {
Username: "",
Password: "",
},
want: nil,
wantErr: true,
err: errors.New("unauthorized: bad credentials"),
want: nil,
wantErr: true,
expectedErr: "unauthorized: bad credentials",
},
}
for _, tt := range tests {
@ -178,7 +180,7 @@ func Test_client_Test(t *testing.T) {
got, err := c.Test()
if tt.wantErr && assert.Error(t, err) {
assert.Equal(t, tt.err, err)
assert.EqualErrorf(t, err, tt.expectedErr, "Error should be: %v, got: %v", tt.wantErr, err)
}
assert.Equal(t, tt.want, got)

View file

@ -5,12 +5,11 @@ import (
"encoding/xml"
"fmt"
"io"
"log"
"net/http"
"net/url"
"time"
"github.com/pkg/errors"
"github.com/autobrr/autobrr/pkg/errors"
)
type Response struct {
@ -69,12 +68,12 @@ func (t *Time) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
err := d.DecodeElement(&raw, &start)
if err != nil {
return err
return errors.Wrap(err, "could not decode element")
}
date, err := time.Parse(time.RFC1123Z, raw)
date, err := time.Parse(time.RFC1123Z, raw)
if err != nil {
return err
return errors.Wrap(err, "could not parse date")
}
*t = Time{date}
@ -115,7 +114,7 @@ func (c *Client) get(endpoint string, opts map[string]string) (int, *Response, e
req, err := http.NewRequest("GET", reqUrl, nil)
if err != nil {
return 0, nil, err
return 0, nil, errors.Wrap(err, "could not build request")
}
if c.UseBasicAuth {
@ -128,19 +127,19 @@ func (c *Client) get(endpoint string, opts map[string]string) (int, *Response, e
resp, err := c.http.Do(req)
if err != nil {
return 0, nil, err
return 0, nil, errors.Wrap(err, "could not make request. %+v", req)
}
defer resp.Body.Close()
var buf bytes.Buffer
if _, err = io.Copy(&buf, resp.Body); err != nil {
return resp.StatusCode, nil, fmt.Errorf("torznab.io.Copy: %w", err)
return resp.StatusCode, nil, errors.Wrap(err, "torznab.io.Copy")
}
var response Response
if err := xml.Unmarshal(buf.Bytes(), &response); err != nil {
return resp.StatusCode, nil, fmt.Errorf("torznab: could not decode feed: %w", err)
return resp.StatusCode, nil, errors.Wrap(err, "torznab: could not decode feed")
}
return resp.StatusCode, &response, nil
@ -149,12 +148,11 @@ func (c *Client) get(endpoint string, opts map[string]string) (int, *Response, e
func (c *Client) GetFeed() ([]FeedItem, error) {
status, res, err := c.get("?t=search", nil)
if err != nil {
//log.Fatalf("error fetching torznab feed: %v", err)
return nil, err
return nil, errors.Wrap(err, "could not get feed")
}
if status != http.StatusOK {
return nil, err
return nil, errors.New("could not get feed")
}
return res.Channel.Items, nil
@ -167,11 +165,11 @@ func (c *Client) Search(query string) ([]FeedItem, error) {
status, res, err := c.get("&t=search&"+params, nil)
if err != nil {
log.Fatalf("error fetching torznab feed: %v", err)
return nil, errors.Wrap(err, "could not search feed")
}
if status != http.StatusOK {
return nil, err
return nil, errors.New("could not search feed")
}
return res.Channel.Items, nil

View file

@ -3,12 +3,11 @@ package whisparr
import (
"bytes"
"encoding/json"
"errors"
"net/http"
"net/url"
"path"
"github.com/rs/zerolog/log"
"github.com/autobrr/autobrr/pkg/errors"
)
func (c *client) get(endpoint string) (*http.Response, error) {
@ -18,8 +17,7 @@ func (c *client) get(endpoint string) (*http.Response, error) {
req, err := http.NewRequest(http.MethodGet, reqUrl, http.NoBody)
if err != nil {
log.Error().Err(err).Msgf("whisparr client request error : %v", reqUrl)
return nil, err
return nil, errors.Wrap(err, "could not build request")
}
if c.config.BasicAuth {
@ -31,8 +29,7 @@ func (c *client) get(endpoint string) (*http.Response, error) {
res, err := c.http.Do(req)
if err != nil {
log.Error().Err(err).Msgf("whisparr client request error : %v", reqUrl)
return nil, err
return nil, errors.Wrap(err, "could not make request: %+v", req)
}
if res.StatusCode == http.StatusUnauthorized {
@ -49,14 +46,12 @@ func (c *client) post(endpoint string, data interface{}) (*http.Response, error)
jsonData, err := json.Marshal(data)
if err != nil {
log.Error().Err(err).Msgf("whisparr client could not marshal data: %v", reqUrl)
return nil, err
return nil, errors.Wrap(err, "could not marshal data: %+v", data)
}
req, err := http.NewRequest(http.MethodPost, reqUrl, bytes.NewBuffer(jsonData))
if err != nil {
log.Error().Err(err).Msgf("whisparr client request error: %v", reqUrl)
return nil, err
return nil, errors.Wrap(err, "could not build request")
}
if c.config.BasicAuth {
@ -69,16 +64,13 @@ func (c *client) post(endpoint string, data interface{}) (*http.Response, error)
res, err := c.http.Do(req)
if err != nil {
log.Error().Err(err).Msgf("whisparr client request error: %v", reqUrl)
return nil, err
return nil, errors.Wrap(err, "could not make request: %+v", req)
}
// validate response
if res.StatusCode == http.StatusUnauthorized {
log.Error().Err(err).Msgf("whisparr client bad request: %v", reqUrl)
return nil, errors.New("unauthorized: bad credentials")
} else if res.StatusCode != http.StatusOK {
log.Error().Err(err).Msgf("whisparr client request error: %v", reqUrl)
return nil, errors.New("whisparr: bad request")
}

View file

@ -3,11 +3,12 @@ package whisparr
import (
"encoding/json"
"io"
"log"
"net/http"
"strings"
"time"
"github.com/rs/zerolog/log"
"github.com/autobrr/autobrr/pkg/errors"
)
type Config struct {
@ -18,6 +19,8 @@ type Config struct {
BasicAuth bool
Username string
Password string
Log *log.Logger
}
type Client interface {
@ -28,6 +31,8 @@ type Client interface {
type client struct {
config Config
http *http.Client
Log *log.Logger
}
func New(config Config) Client {
@ -39,6 +44,11 @@ func New(config Config) Client {
c := &client{
config: config,
http: httpClient,
Log: config.Log,
}
if config.Log == nil {
c.Log = log.New(io.Discard, "", log.LstdFlags)
}
return c
@ -68,26 +78,23 @@ type SystemStatusResponse struct {
func (c *client) Test() (*SystemStatusResponse, error) {
res, err := c.get("system/status")
if err != nil {
log.Error().Stack().Err(err).Msg("whisparr client get error")
return nil, err
return nil, errors.Wrap(err, "could not test whisparr")
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
log.Error().Stack().Err(err).Msg("whisparr client error reading body")
return nil, err
return nil, errors.Wrap(err, "could not read body")
}
response := SystemStatusResponse{}
err = json.Unmarshal(body, &response)
if err != nil {
log.Error().Stack().Err(err).Msg("whisparr client error json unmarshal")
return nil, err
return nil, errors.Wrap(err, "could not unmarshal data")
}
log.Trace().Msgf("whisparr system/status response: %+v", response)
c.Log.Printf("whisparr system/status status: (%v) response: %v\n", res.Status, string(body))
return &response, nil
}
@ -95,8 +102,7 @@ func (c *client) Test() (*SystemStatusResponse, error) {
func (c *client) Push(release Release) ([]string, error) {
res, err := c.post("release/push", release)
if err != nil {
log.Error().Stack().Err(err).Msg("whisparr client post error")
return nil, err
return nil, errors.Wrap(err, "could not push release to whisparr: %+v", release)
}
if res == nil {
@ -107,24 +113,22 @@ func (c *client) Push(release Release) ([]string, error) {
body, err := io.ReadAll(res.Body)
if err != nil {
log.Error().Stack().Err(err).Msg("whisparr client error reading body")
return nil, err
return nil, errors.Wrap(err, "could not read body")
}
pushResponse := make([]PushResponse, 0)
err = json.Unmarshal(body, &pushResponse)
if err != nil {
log.Error().Stack().Err(err).Msg("whisparr client error json unmarshal")
return nil, err
return nil, errors.Wrap(err, "could not unmarshal data")
}
log.Trace().Msgf("whisparr release/push response body: %+v", string(body))
c.Log.Printf("whisparr release/push status: (%v) response: %v\n", res.Status, string(body))
// log and return if rejected
if pushResponse[0].Rejected {
rejections := strings.Join(pushResponse[0].Rejections, ", ")
log.Trace().Msgf("whisparr push rejected: %s - reasons: %q", release.Title, rejections)
c.Log.Printf("whisparr release/push rejected %v reasons: %q\n", release.Title, rejections)
return pushResponse[0].Rejections, nil
}