pvr: add options and support for anime (#17)

* pvr: begin adding options

* pvr: add ability to configure add behaviour via config

* pvr: add skip_anime

* pvr: do not continue processing item if lookup failed or add to pvr failed
This commit is contained in:
l3uddz
2021-02-20 20:01:04 +00:00
committed by GitHub
parent 523e561666
commit a2848439b9
14 changed files with 283 additions and 30 deletions

View File

@@ -53,9 +53,28 @@ media:
client_id: trakt-client-id
omdb:
api_key: omdb-api-key
tvdb:
api_key: tvdb-legacy-api-key
```
An omdb `api_key` can also be provided which will be used to supplement trakt data with additional information such as IMDb rating, Metascore rating and Rotten Tomatoes rating.
An omdb `api_key` can be provided which will be used to supplement trakt data with additional information such as:
- Metascore
- RottenTomatoes
- ImdbRating
- ImdbVotes
- Language
- Country
An tvdb `api_key` can be provided which will be used to supplement trakt data with additional information such as:
- Runtime
- Language
- Genre
- AirsDayOfWeek
- SiteRating
- SiteRatingCount
### PVR
@@ -69,6 +88,10 @@ pvrs:
api_key: sonarr-api-key
quality_profile: WEBDL-1080p
root_folder: /mnt/unionfs/Media/TV
options:
add_monitored: true
search_missing: true
skip_anime: true
filters:
ignores:
- 'not (FeedTitle matches "(?i)S\\d\\d?E?\\d?\\d?")'
@@ -92,6 +115,15 @@ pvrs:
- 'TvdbId in ["248783"]'
```
The following `options` can be set to override the default behaviour when adding content to a PVR (types: Sonarr & Radarr).
- `add_monitored` (default: `true`) - Add new content as monitored
- `search_missing` (default: `true`) - Add new content and search immediately
The following `options` can be set to skip adding content to a Sonarr PVR.
- `skip_anime` (default: `true`) - If the series is of the anime type, do not add it
### RSS
The rss configuration section is where you will specify the RSS feeds that Nabarr will work with.
@@ -122,6 +154,8 @@ media:
client_id: trakt-client-id
omdb:
api_key: omdb-api-key
tvdb:
api_key: tvdb-legacy-api-key
pvrs:
- name: sonarr
type: sonarr
@@ -133,14 +167,17 @@ pvrs:
ignores:
- 'not (FeedTitle matches "(?i)S\\d\\d?E?\\d?\\d?")'
- 'FeedTitle matches "(?i)\\d\\d\\d\\d\\s?[\\s\\.\\-]\\d\\d?\\s?[\\s\\.\\-]\\d\\d?"'
- 'len(Languages) != 1 || "en" not in Languages'
- 'not (any(Country, {# in ["us", "gb", "au", "ca", "nz"]})) && not (any(["USA", "UK", "Australia", "Canada", "New Zealand"], {Omdb.Country == #}))'
- 'len(Languages) > 0 && not (any(Languages, {# in ["en", ""]}))'
- 'Omdb.Language != "" && Omdb.Language != "English"'
- 'Tvdb.Language != "" && Tvdb.Language != "en"'
- 'not (any(Languages, {# in ["en", ""]})) && Omdb.Language == "" && Tvdb.Language == ""'
- 'Runtime < 10 || Runtime > 70'
- 'Network == ""'
- 'any (["Hallmark Movies"], {Network contains #})'
- 'not (any(Country, {# in ["us", "gb", "au", "ca"]}))'
- 'Year < 2000'
- 'Year < 2021 && Omdb.ImdbRating < 7.5'
- 'AiredEpisodes > 200'
- 'AiredEpisodes > 100'
- 'Year > (Now().Year() + 1)'
- 'any (["WWE", "AEW", "WWF", "NXT", "Live:", "Concert", "Musical", " Edition", "Wrestling"], {Title contains #})'
- 'len(Genres) == 0'
@@ -158,7 +195,8 @@ pvrs:
root_folder: /mnt/unionfs/Media/Movies
filters:
ignores:
- 'len(Languages) != 1 || "en" not in Languages'
- 'not (any(Country, {# in ["us", "gb", "au", "ca", "nz"]})) && not (any(["USA", "UK", "Australia", "Canada", "New Zealand"], {Omdb.Country == #}))'
- 'not (any(Languages, {# in ["en"]})) && Omdb.Language != "English"'
- 'Runtime < 60'
- 'len(Genres) == 0'
- '("music" in Genres || "documentary" in Genres)'

View File

@@ -14,7 +14,7 @@ import (
type PVR interface {
Type() string
GetFiltersHash() string
AddMediaItem(*media.Item) error
AddMediaItem(*media.Item, ...nabarr.PvrOption) error
ShouldIgnore(*media.Item) (bool, string, error)
Start() state.State
QueueFeedItem(*media.FeedItem)

71
pvr.go
View File

@@ -1,19 +1,70 @@
package nabarr
import "time"
import (
"time"
)
/* pvr config / filters */
type PvrConfig struct {
Name string `yaml:"name"`
Type string `yaml:"type"`
URL string `yaml:"url"`
ApiKey string `yaml:"api_key"`
QualityProfile string `yaml:"quality_profile"`
RootFolder string `yaml:"root_folder"`
Filters PvrFilters `yaml:"filters"`
CacheDuration time.Duration `yaml:"cache_duration"`
Verbosity string `yaml:"verbosity,omitempty"`
Name string `yaml:"name"`
Type string `yaml:"type"`
URL string `yaml:"url"`
ApiKey string `yaml:"api_key"`
QualityProfile string `yaml:"quality_profile"`
RootFolder string `yaml:"root_folder"`
Options struct {
// add options
AddMonitored *bool `yaml:"add_monitored"`
SearchMissing *bool `yaml:"search_missing"`
// skip objects
SkipAnime *bool `yaml:"skip_anime"`
} `yaml:"options"`
Filters PvrFilters `yaml:"filters"`
CacheDuration time.Duration `yaml:"cache_duration"`
Verbosity string `yaml:"verbosity,omitempty"`
}
type PvrFilters struct {
Ignores []string
}
/* pvr options */
type PvrOption func(options *PvrOptions)
type PvrOptions struct {
// the seriesType returned from the lookup before adding (sonarr)
LookupType string
AddMonitored bool
SearchMissing bool
}
func BuildPvrOptions(opts ...PvrOption) (*PvrOptions, error) {
os := &PvrOptions{}
for _, opt := range opts {
opt(os)
}
return os, nil
}
func WithSeriesType(seriesType string) PvrOption {
return func(opts *PvrOptions) {
opts.LookupType = seriesType
}
}
func WithAddMonitored(monitored bool) PvrOption {
return func(opts *PvrOptions) {
opts.AddMonitored = monitored
}
}
func WithSearchMissing(missing bool) PvrOption {
return func(opts *PvrOptions) {
opts.SearchMissing = missing
}
}

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/l3uddz/nabarr"
"github.com/l3uddz/nabarr/media"
"github.com/l3uddz/nabarr/util"
"github.com/lucperkins/rek"
@@ -122,7 +123,13 @@ func (c *Client) lookupMediaItem(item *media.Item) (*lookupRequest, error) {
return nil, fmt.Errorf("movie lookup %sId: %v: %w", mdType, mdId, ErrItemNotFound)
}
func (c *Client) AddMediaItem(item *media.Item) error {
func (c *Client) AddMediaItem(item *media.Item, opts ...nabarr.PvrOption) error {
// prepare options
o, err := nabarr.BuildPvrOptions(opts...)
if err != nil {
return fmt.Errorf("build options: %v: %w", item.TmdbId, err)
}
// prepare request
req := addRequest{
Title: item.Title,
@@ -130,11 +137,11 @@ func (c *Client) AddMediaItem(item *media.Item) error {
Year: item.Year,
QualityProfileId: c.qualityProfileId,
Images: []string{},
Monitored: true,
Monitored: o.AddMonitored,
RootFolderPath: c.rootFolder,
MinimumAvailability: "released",
AddOptions: addOptions{
SearchForMovie: true,
SearchForMovie: o.SearchMissing,
IgnoreEpisodesWithFiles: false,
IgnoreEpisodesWithoutFiles: false,
},

View File

@@ -3,6 +3,7 @@ package radarr
import (
"errors"
"fmt"
"github.com/l3uddz/nabarr"
"github.com/l3uddz/nabarr/media"
"github.com/lefelys/state"
)
@@ -144,6 +145,7 @@ func (c *Client) queueProcessor(tail state.ShutdownTail) {
Str("feed_imdb_id", feedItem.ImdbId).
Str("feed_name", feedItem.Feed).
Msg("Failed finding item via pvr lookup")
continue
}
if s.Id > 0 {
@@ -184,7 +186,6 @@ func (c *Client) queueProcessor(tail state.ShutdownTail) {
if c.testMode {
c.log.Info().
Err(err).
Str("trakt_title", mediaItem.Title).
Str("trakt_imdb_id", mediaItem.ImdbId).
Str("trakt_tmdb_id", mediaItem.TmdbId).
@@ -194,7 +195,12 @@ func (c *Client) queueProcessor(tail state.ShutdownTail) {
continue
}
if err := c.AddMediaItem(mediaItem); err != nil {
opts := []nabarr.PvrOption{
nabarr.WithAddMonitored(c.addMonitored),
nabarr.WithSearchMissing(c.searchMissing),
}
if err := c.AddMediaItem(mediaItem, opts...); err != nil {
c.log.Error().
Err(err).
Str("feed_title", mediaItem.FeedTitle).
@@ -204,6 +210,7 @@ func (c *Client) queueProcessor(tail state.ShutdownTail) {
Int("trakt_year", mediaItem.Year).
Str("feed_name", feedItem.Feed).
Msg("Failed adding item to pvr")
continue
}
// add item to perm cache (item was added to pvr)
@@ -216,7 +223,6 @@ func (c *Client) queueProcessor(tail state.ShutdownTail) {
}
c.log.Info().
Err(err).
Str("trakt_title", mediaItem.Title).
Str("trakt_imdb_id", mediaItem.ImdbId).
Str("trakt_tmdb_id", mediaItem.TmdbId).

View File

@@ -20,6 +20,10 @@ type Client struct {
rootFolder string
qualityProfileId int
// options
searchMissing bool
addMonitored bool
apiURL string
apiHeaders map[string]string
apiTimeout time.Duration
@@ -65,7 +69,9 @@ func New(c nabarr.PvrConfig, mode string, m *media.Client, cc *cache.Client) (*C
name: strings.ToLower(c.Name),
testMode: strings.EqualFold(mode, "test"),
rootFolder: c.RootFolder,
rootFolder: c.RootFolder,
searchMissing: util.BoolOrDefault(c.Options.SearchMissing, true),
addMonitored: util.BoolOrDefault(c.Options.AddMonitored, true),
cache: cc,
cacheTempDuration: c.CacheDuration,

View File

@@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/l3uddz/nabarr"
"github.com/l3uddz/nabarr/media"
"github.com/l3uddz/nabarr/util"
"github.com/lucperkins/rek"
@@ -105,7 +106,13 @@ func (c *Client) lookupMediaItem(item *media.Item) (*lookupRequest, error) {
return nil, fmt.Errorf("series lookup tvdbId: %v: %w", item.TvdbId, ErrItemNotFound)
}
func (c *Client) AddMediaItem(item *media.Item) error {
func (c *Client) AddMediaItem(item *media.Item, opts ...nabarr.PvrOption) error {
// prepare options
o, err := nabarr.BuildPvrOptions(opts...)
if err != nil {
return fmt.Errorf("build options: %v: %w", item.TvdbId, err)
}
// prepare request
tvdbId, err := strconv.Atoi(item.TvdbId)
if err != nil {
@@ -119,15 +126,15 @@ func (c *Client) AddMediaItem(item *media.Item) error {
QualityProfileId: c.qualityProfileId,
Images: []string{},
Tags: []string{},
Monitored: true,
Monitored: o.AddMonitored,
RootFolderPath: c.rootFolder,
AddOptions: addOptions{
SearchForMissingEpisodes: true,
SearchForMissingEpisodes: o.SearchMissing,
IgnoreEpisodesWithFiles: false,
IgnoreEpisodesWithoutFiles: false,
},
Seasons: []string{},
SeriesType: "standard",
SeriesType: util.StringOrDefault(o.LookupType, "standard"),
SeasonFolder: true,
TvdbId: tvdbId,
}

View File

@@ -3,8 +3,11 @@ package sonarr
import (
"errors"
"fmt"
"github.com/l3uddz/nabarr"
"github.com/l3uddz/nabarr/media"
"github.com/l3uddz/nabarr/util"
"github.com/lefelys/state"
"strings"
)
func (c *Client) QueueFeedItem(item *media.FeedItem) {
@@ -134,6 +137,7 @@ func (c *Client) queueProcessor(tail state.ShutdownTail) {
Str("feed_tvdb_id", feedItem.TvdbId).
Str("feed_name", feedItem.Feed).
Msg("Failed finding item via pvr lookup")
continue
}
if s.Id > 0 {
@@ -156,6 +160,23 @@ func (c *Client) queueProcessor(tail state.ShutdownTail) {
continue
}
// set appropriate series type
switch {
case util.StringSliceContains(mediaItem.Genres, "anime"), util.StringSliceContains(mediaItem.Tvdb.Genre, "anime"):
s.Type = "anime"
}
// check if item should be skipped (skip options)
if c.skipAnime && strings.EqualFold(s.Type, "anime") {
c.log.Debug().
Str("trakt_title", mediaItem.Title).
Str("trakt_tvdb_id", mediaItem.TvdbId).
Int("trakt_year", mediaItem.Year).
Str("feed_name", feedItem.Feed).
Msg("Skipping item (skip_anime enabled)")
continue
}
// add item to pvr
c.log.Debug().
Str("feed_title", mediaItem.FeedTitle).
@@ -172,7 +193,6 @@ func (c *Client) queueProcessor(tail state.ShutdownTail) {
if c.testMode {
c.log.Info().
Err(err).
Str("trakt_title", mediaItem.Title).
Str("trakt_tvdb_id", mediaItem.TvdbId).
Int("trakt_year", mediaItem.Year).
@@ -181,7 +201,13 @@ func (c *Client) queueProcessor(tail state.ShutdownTail) {
continue
}
if err := c.AddMediaItem(mediaItem); err != nil {
opts := []nabarr.PvrOption{
nabarr.WithSeriesType(s.Type),
nabarr.WithAddMonitored(c.addMonitored),
nabarr.WithSearchMissing(c.searchMissing),
}
if err := c.AddMediaItem(mediaItem, opts...); err != nil {
c.log.Error().
Err(err).
Str("feed_title", mediaItem.FeedTitle).
@@ -190,6 +216,7 @@ func (c *Client) queueProcessor(tail state.ShutdownTail) {
Int("trakt_year", mediaItem.Year).
Str("feed_name", feedItem.Feed).
Msg("Failed adding item to pvr")
continue
}
// add item to perm cache (item was added to pvr)
@@ -202,7 +229,6 @@ func (c *Client) queueProcessor(tail state.ShutdownTail) {
}
c.log.Info().
Err(err).
Str("trakt_title", mediaItem.Title).
Str("trakt_tvdb_id", mediaItem.TvdbId).
Int("trakt_year", mediaItem.Year).

View File

@@ -20,6 +20,11 @@ type Client struct {
rootFolder string
qualityProfileId int
// options
searchMissing bool
addMonitored bool
skipAnime bool
apiURL string
apiHeaders map[string]string
apiTimeout time.Duration
@@ -67,6 +72,10 @@ func New(c nabarr.PvrConfig, mode string, m *media.Client, cc *cache.Client) (*C
rootFolder: c.RootFolder,
searchMissing: util.BoolOrDefault(c.Options.SearchMissing, true),
addMonitored: util.BoolOrDefault(c.Options.AddMonitored, true),
skipAnime: util.BoolOrDefault(c.Options.SkipAnime, true),
cache: cc,
cacheTempDuration: c.CacheDuration,
cacheFiltersHash: util.AsSHA256(c.Filters),

View File

@@ -15,6 +15,7 @@ type lookupRequest struct {
TitleSlug string `json:"titleSlug"`
Year int `json:"year,omitempty"`
TvdbId int `json:"tvdbId"`
Type string `json:"seriesType"`
}
type addRequest struct {

View File

@@ -20,3 +20,18 @@ func Atof64(val string, defaultVal float64) float64 {
}
return n
}
func StringOrDefault(val string, defaultVal string) string {
if val == "" {
return defaultVal
}
return val
}
func BoolOrDefault(val *bool, defaultVal bool) bool {
if val == nil {
return defaultVal
}
return *val
}

View File

@@ -81,3 +81,39 @@ func TestAtoi(t *testing.T) {
})
}
}
func TestBoolOrDefault(t *testing.T) {
type args struct {
val *bool
defaultVal bool
}
tests := []struct {
name string
args args
want bool
}{
{
name: "expect default",
args: args{
val: nil,
defaultVal: true,
},
want: true,
},
{
name: "expect value",
args: args{
val: new(bool),
defaultVal: true,
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := BoolOrDefault(tt.args.val, tt.args.defaultVal); got != tt.want {
t.Errorf("BoolOrDefault() = %v, want %v", got, tt.want)
}
})
}
}

12
util/slice.go Normal file
View File

@@ -0,0 +1,12 @@
package util
import "strings"
func StringSliceContains(slice []string, val string) bool {
for _, s := range slice {
if strings.EqualFold(s, val) {
return true
}
}
return false
}

39
util/slice_test.go Normal file
View File

@@ -0,0 +1,39 @@
package util
import "testing"
func TestStringSliceContains(t *testing.T) {
type args struct {
slice []string
val string
}
tests := []struct {
name string
args args
want bool
}{
{
name: "expect true",
args: args{
slice: []string{"tes", "Test"},
val: "test",
},
want: true,
},
{
name: "expect false",
args: args{
slice: []string{"tes", "Test"},
val: "testing",
},
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := StringSliceContains(tt.args.slice, tt.args.val); got != tt.want {
t.Errorf("StringSliceContains() = %v, want %v", got, tt.want)
}
})
}
}