Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds sonarr custom formats and fixes quality defs. #57

Merged
merged 35 commits into from
Sep 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
9cf530a
add missing delete methods, and update a few wrrors
davidnewhall Jul 3, 2022
4ee7ad4
fix lint issues
davidnewhall Jul 3, 2022
a4d8bf0
Merge branch 'master' of github.com:golift/starr
davidnewhall Sep 2, 2022
ddf8c65
Change interface, change debug logging
davidnewhall Sep 2, 2022
a5a408e
fix what I broke
davidnewhall Sep 2, 2022
5fb473c
add back the test
davidnewhall Sep 2, 2022
7e3e927
re-export methods, and update deps
davidnewhall Sep 2, 2022
8625234
fix linter
davidnewhall Sep 2, 2022
2230da3
remove timeout and ssl from config, use Client()
davidnewhall Sep 2, 2022
b65a7bb
A few fixes/updates
davidnewhall Sep 2, 2022
dbc17c6
quality defs and custom formats: radarr and sonarr
davidnewhall Sep 2, 2022
3e41c55
Add more comments
davidnewhall Sep 2, 2022
f6b0ae1
bug fix
davidnewhall Sep 2, 2022
dda37dd
Merge branch 'dn2_change_debug' into dn2_more_endpoints
davidnewhall Sep 2, 2022
9bcb442
make Req method work with non-api paths
davidnewhall Sep 2, 2022
3d430b5
Merge branch 'dn2_change_debug' into dn2_more_endpoints
davidnewhall Sep 2, 2022
63f0b78
allow non-api paths
davidnewhall Sep 2, 2022
5cc32aa
Merge branch 'dn2_change_debug' into dn2_more_endpoints
davidnewhall Sep 2, 2022
0479d91
add missing bodies
davidnewhall Sep 2, 2022
972eb0f
Merge branch 'dn2_change_debug' into dn2_more_endpoints
davidnewhall Sep 2, 2022
ae96ec4
restrict debug output to json payloads
davidnewhall Sep 2, 2022
b3d1a4d
Merge branch 'dn2_change_debug' into dn2_more_endpoints
davidnewhall Sep 2, 2022
02e1b03
fix qualities
davidnewhall Sep 3, 2022
492ee32
use bas quality for quality defs
davidnewhall Sep 3, 2022
9eb56a8
update all defs at once
davidnewhall Sep 3, 2022
5242608
Add non-200 error data
davidnewhall Sep 3, 2022
879511c
ignore output on delete requests
davidnewhall Sep 4, 2022
0d5c341
Merge branch 'dn2_change_debug' into dn2_more_endpoints
davidnewhall Sep 4, 2022
1bde41d
merge
davidnewhall Sep 4, 2022
b6c26a1
merge
davidnewhall Sep 4, 2022
b121085
fix quality profiles
davidnewhall Sep 5, 2022
142e8a5
Update shared.go
davidnewhall Sep 5, 2022
f242882
do not remove updatequalitydefinition
davidnewhall Sep 5, 2022
8cd7bb0
Merge branch 'dn2_more_endpoints' of github.com:golift/starr into dn2…
davidnewhall Sep 5, 2022
cd08067
fix up a few more misses
davidnewhall Sep 5, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 33 additions & 3 deletions http.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package starr
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"net/http"
Expand Down Expand Up @@ -69,14 +70,43 @@ func (c *Config) req(
}

if resp.StatusCode < 200 || resp.StatusCode > 299 {
closeResp(resp)
return nil, fmt.Errorf("failed: %v (status: %s): %w",
req.RequestURI, resp.Status, ErrInvalidStatusCode)
return nil, parseNon200(req, resp)
}

return resp, nil
}

// parseNon200 attempts to extract an error message from a non-200 response.
func parseNon200(req *http.Request, resp *http.Response) error {
defer resp.Body.Close()

var msg struct {
Msg string `json:"message"`
}

reply, err := io.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("failed: %v (status: %s): %w",
req.RequestURI, resp.Status, ErrInvalidStatusCode)
}

if err := json.Unmarshal(reply, &msg); err == nil {
return fmt.Errorf("failed: %v (status: %s): %w: %s",
req.RequestURI, resp.Status, ErrInvalidStatusCode, msg.Msg)
}

const maxSize = 200 // arbitrary max size

replyStr := string(reply)
if len(replyStr) > maxSize {
return fmt.Errorf("failed: %v (status: %s): %w: %s",
req.RequestURI, resp.Status, ErrInvalidStatusCode, replyStr[:maxSize])
}

return fmt.Errorf("failed: %v (status: %s): %w: %s",
req.RequestURI, resp.Status, ErrInvalidStatusCode, replyStr)
}

func closeResp(resp *http.Response) {
if resp != nil && resp.Body != nil {
_, _ = io.ReadAll(resp.Body)
Expand Down
9 changes: 4 additions & 5 deletions radarr/customformat.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import (
"encoding/json"
"fmt"
"path"
"strconv"
)

const bpCustomFormat = APIver + "/customFormat"
Expand Down Expand Up @@ -100,9 +99,9 @@ func (r *Radarr) UpdateCustomFormatContext(ctx context.Context, format *CustomFo

var output CustomFormat

uri := path.Join(bpCustomFormat, strconv.Itoa(cfID))
uri := path.Join(bpCustomFormat, fmt.Sprint(cfID))
if err := r.PutInto(ctx, uri, nil, &body, &output); err != nil {
return nil, fmt.Errorf("api.Put(%s): %w", bpCustomFormat, err)
return nil, fmt.Errorf("api.Put(%s): %w", uri, err)
}

return &output, nil
Expand All @@ -115,9 +114,9 @@ func (r *Radarr) DeleteCustomFormat(cfID int) error {

// DeleteCustomFormatContext deletes a custom format.
func (r *Radarr) DeleteCustomFormatContext(ctx context.Context, cfID int) error {
uri := path.Join(bpCustomFormat, strconv.Itoa(cfID))
uri := path.Join(bpCustomFormat, fmt.Sprint(cfID))
if err := r.DeleteAny(ctx, uri, nil); err != nil {
return fmt.Errorf("api.Delete(%s): %w", bpCustomFormat, err)
return fmt.Errorf("api.Delete(%s): %w", uri, err)
}

return nil
Expand Down
108 changes: 108 additions & 0 deletions radarr/qualitydefinition.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package radarr

import (
"bytes"
"context"
"encoding/json"
"fmt"
"path"

"golift.io/starr"
)

// QualityDefinition is the /api/v3/qualitydefinition endpoint.
type QualityDefinition struct {
ID int64 `json:"id,omitempty"`
Weight int64 `json:"weight"` // This should not be changed.
MinSize float64 `json:"minSize"`
MaxSize float64 `json:"maxSize"`
PrefSize float64 `json:"preferredSize"`
Title string `json:"title"`
Quality *starr.BaseQuality `json:"quality"`
}

// Define Base Path for Quality Definition calls.
const bpQualityDefinition = APIver + "/qualityDefinition"

// GetQualityDefinitions returns all configured quality definitions.
func (r *Radarr) GetQualityDefinitions() ([]*QualityDefinition, error) {
return r.GetQualityDefinitionsContext(context.Background())
}

// GetQualityDefinitionsContext returns all configured quality definitions.
func (r *Radarr) GetQualityDefinitionsContext(ctx context.Context) ([]*QualityDefinition, error) {
var output []*QualityDefinition

if err := r.GetInto(ctx, bpQualityDefinition, nil, &output); err != nil {
return nil, fmt.Errorf("api.Get(%s): %w", bpQualityDefinition, err)
}

return output, nil
}

// GetQualityDefinition returns a single quality definition.
func (r *Radarr) GetQualityDefinition(qualityDefinitionID int64) (*QualityDefinition, error) {
return r.GetQualityDefinitionContext(context.Background(), qualityDefinitionID)
}

// GetQualityDefinitionContext returns a single quality definition.
func (r *Radarr) GetQualityDefinitionContext(ctx context.Context, qdID int64) (*QualityDefinition, error) {
var output QualityDefinition

uri := path.Join(bpQualityDefinition, fmt.Sprint(qdID))
if err := r.GetInto(ctx, uri, nil, &output); err != nil {
return nil, fmt.Errorf("api.Get(%s): %w", uri, err)
}

return &output, nil
}

// UpdateQualityDefinition updates a quality definition.
func (r *Radarr) UpdateQualityDefinition(definition *QualityDefinition) (*QualityDefinition, error) {
return r.UpdateQualityDefinitionContext(context.Background(), definition)
}

// UpdateQualityDefinitionContext updates a quality definition.
func (r *Radarr) UpdateQualityDefinitionContext(
ctx context.Context,
definition *QualityDefinition,
) (*QualityDefinition, error) {
var output QualityDefinition

var body bytes.Buffer
if err := json.NewEncoder(&body).Encode(definition); err != nil {
return nil, fmt.Errorf("json.Marshal(%s): %w", bpQualityDefinition, err)
}

uri := path.Join(bpQualityDefinition, fmt.Sprint(definition.ID))
if err := r.PutInto(ctx, uri, nil, &body, &output); err != nil {
return nil, fmt.Errorf("api.Put(%s): %w", uri, err)
}

return &output, nil
}

// UpdateQualityDefinitions updates all quality definitions.
func (r *Radarr) UpdateQualityDefinitions(definition []*QualityDefinition) ([]*QualityDefinition, error) {
return r.UpdateQualityDefinitionsContext(context.Background(), definition)
}

// UpdateQualityDefinitionsContext updates all quality definitions.
func (r *Radarr) UpdateQualityDefinitionsContext(
ctx context.Context,
definition []*QualityDefinition,
) ([]*QualityDefinition, error) {
var output []*QualityDefinition

var body bytes.Buffer
if err := json.NewEncoder(&body).Encode(definition); err != nil {
return nil, fmt.Errorf("json.Marshal(%s): %w", bpQualityDefinition, err)
}

uri := path.Join(bpQualityDefinition, "update")
if err := r.PutInto(ctx, uri, nil, &body, &output); err != nil {
return nil, fmt.Errorf("api.Put(%s): %w", uri, err)
}

return output, nil
}
25 changes: 9 additions & 16 deletions radarr/qualityprofile.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,22 +15,15 @@ const bpQualityProfile = APIver + "/qualityProfile"

// QualityProfile is applied to Movies.
type QualityProfile struct {
ID int64 `json:"id"`
Name string `json:"name"`
UpgradeAllowed bool `json:"upgradeAllowed"`
Cutoff int64 `json:"cutoff"`
Qualities []*starr.Quality `json:"items"`
MinFormatScore int64 `json:"minFormatScore"`
CutoffFormatScore int64 `json:"cutoffFormatScore"`
FormatItems []*FormatItem `json:"formatItems,omitempty"`
Language *starr.Value `json:"language"`
}

// FormatItem is part of a QualityProfile.
type FormatItem struct {
Format int `json:"format"`
Name string `json:"name"`
Score int `json:"score"`
ID int64 `json:"id"`
Name string `json:"name"`
UpgradeAllowed bool `json:"upgradeAllowed"`
Cutoff int64 `json:"cutoff"`
Qualities []*starr.Quality `json:"items"`
MinFormatScore int64 `json:"minFormatScore"`
CutoffFormatScore int64 `json:"cutoffFormatScore"`
FormatItems []*starr.FormatItem `json:"formatItems,omitempty"`
Language *starr.Value `json:"language"`
}

// GetQualityProfiles returns all configured quality profiles.
Expand Down
7 changes: 7 additions & 0 deletions shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,13 @@ type PlayTime struct {
time.Duration
}

// FormatItem is part of a quality profile.
type FormatItem struct {
Format int64 `json:"format"`
Name string `json:"name"`
Score int64 `json:"score"`
}

// UnmarshalJSON parses a run time duration in format hh:mm:ss.
func (d *PlayTime) UnmarshalJSON(b []byte) error {
d.Original = strings.Trim(string(b), `"'`)
Expand Down
134 changes: 134 additions & 0 deletions sonarr/customformat.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package sonarr

import (
"bytes"
"context"
"encoding/json"
"fmt"
"path"
)

/* Custom Formats do not exist in Sonarr v3; this is v4 only. */

const bpCustomFormat = APIver + "/customFormat"

// CustomFormat is the api/customformat endpoint payload.
// This data and these endpoints do not exist in Sonarr v3; this is v4 only.
type CustomFormat struct {
ID int `json:"id"`
Name string `json:"name"`
IncludeCFWhenRenaming bool `json:"includeCustomFormatWhenRenaming"`
Specifications []*CustomFormatSpec `json:"specifications"`
}

// CustomFormatSpec is part of a CustomFormat.
type CustomFormatSpec struct {
Name string `json:"name"`
Implementation string `json:"implementation"`
Implementationname string `json:"implementationName"`
Infolink string `json:"infoLink"`
Negate bool `json:"negate"`
Required bool `json:"required"`
Fields []*CustomFormatField `json:"fields"`
}

// CustomFormatField is part of a CustomFormat Specification.
type CustomFormatField struct {
Order int `json:"order"`
Name string `json:"name"`
Label string `json:"label"`
Value interface{} `json:"value"` // should be a string, but sometimes it's a number.
Type string `json:"type"`
Advanced bool `json:"advanced"`
}

// GetCustomFormats returns all configured Custom Formats.
// This data and these endpoints do not exist in Sonarr v3; this is v4 only.
func (s *Sonarr) GetCustomFormats() ([]*CustomFormat, error) {
return s.GetCustomFormatsContext(context.Background())
}

// GetCustomFormatsContext returns all configured Custom Formats.
// This data and these endpoints do not exist in Sonarr v3; this is v4 only.
func (s *Sonarr) GetCustomFormatsContext(ctx context.Context) ([]*CustomFormat, error) {
var output []*CustomFormat
if err := s.GetInto(ctx, bpCustomFormat, nil, &output); err != nil {
return nil, fmt.Errorf("api.Get(%s): %w", bpCustomFormat, err)
}

return output, nil
}

// AddCustomFormat creates a new custom format and returns the response (with ID).
// This data and these endpoints do not exist in Sonarr v3; this is v4 only.
func (s *Sonarr) AddCustomFormat(format *CustomFormat) (*CustomFormat, error) {
return s.AddCustomFormatContext(context.Background(), format)
}

// AddCustomFormatContext creates a new custom format and returns the response (with ID).
// This data and these endpoints do not exist in Sonarr v3; this is v4 only.
func (s *Sonarr) 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.

var body bytes.Buffer
if err := json.NewEncoder(&body).Encode(format); err != nil {
return nil, fmt.Errorf("json.Marshal(%s): %w", bpCustomFormat, err)
}

if err := s.PostInto(ctx, bpCustomFormat, nil, &body, &output); err != nil {
return nil, fmt.Errorf("api.Post(%s): %w", bpCustomFormat, err)
}

return &output, nil
}

// UpdateCustomFormat updates an existing custom format and returns the response.
// This data and these endpoints do not exist in Sonarr v3; this is v4 only.
func (s *Sonarr) UpdateCustomFormat(cf *CustomFormat, cfID int) (*CustomFormat, error) {
return s.UpdateCustomFormatContext(context.Background(), cf, cfID)
}

// UpdateCustomFormatContext updates an existing custom format and returns the response.
// This data and these endpoints do not exist in Sonarr v3; this is v4 only.
func (s *Sonarr) UpdateCustomFormatContext(ctx context.Context, format *CustomFormat, cfID int) (*CustomFormat, error) {
if cfID == 0 {
cfID = format.ID
}

var body bytes.Buffer
if err := json.NewEncoder(&body).Encode(format); err != nil {
return nil, fmt.Errorf("json.Marshal(%s): %w", bpCustomFormat, err)
}

var output CustomFormat

uri := path.Join(bpCustomFormat, fmt.Sprint(cfID))
if err := s.PutInto(ctx, uri, nil, &body, &output); err != nil {
return nil, fmt.Errorf("api.Put(%s): %w", uri, err)
}

return &output, nil
}

// DeleteCustomFormat deletes a custom format.
// This data and these endpoints do not exist in Sonarr v3; this is v4 only.
func (s *Sonarr) DeleteCustomFormat(cfID int) error {
return s.DeleteCustomFormatContext(context.Background(), cfID)
}

// DeleteCustomFormatContext deletes a custom format.
// This data and these endpoints do not exist in Sonarr v3; this is v4 only.
func (s *Sonarr) DeleteCustomFormatContext(ctx context.Context, cfID int) error {
uri := path.Join(bpCustomFormat, fmt.Sprint(cfID))
if err := s.DeleteAny(ctx, uri, nil); err != nil {
return fmt.Errorf("api.Delete(%s): %w", uri, err)
}

return nil
}
Loading