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 client_id: trakt-client-id
omdb: omdb:
api_key: omdb-api-key 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 ### PVR
@@ -69,6 +88,10 @@ pvrs:
api_key: sonarr-api-key api_key: sonarr-api-key
quality_profile: WEBDL-1080p quality_profile: WEBDL-1080p
root_folder: /mnt/unionfs/Media/TV root_folder: /mnt/unionfs/Media/TV
options:
add_monitored: true
search_missing: true
skip_anime: true
filters: filters:
ignores: ignores:
- 'not (FeedTitle matches "(?i)S\\d\\d?E?\\d?\\d?")' - 'not (FeedTitle matches "(?i)S\\d\\d?E?\\d?\\d?")'
@@ -92,6 +115,15 @@ pvrs:
- 'TvdbId in ["248783"]' - '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 ### RSS
The rss configuration section is where you will specify the RSS feeds that Nabarr will work with. 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 client_id: trakt-client-id
omdb: omdb:
api_key: omdb-api-key api_key: omdb-api-key
tvdb:
api_key: tvdb-legacy-api-key
pvrs: pvrs:
- name: sonarr - name: sonarr
type: sonarr type: sonarr
@@ -133,14 +167,17 @@ pvrs:
ignores: ignores:
- 'not (FeedTitle matches "(?i)S\\d\\d?E?\\d?\\d?")' - '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?"' - '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' - 'Runtime < 10 || Runtime > 70'
- 'Network == ""' - 'Network == ""'
- 'any (["Hallmark Movies"], {Network contains #})' - 'any (["Hallmark Movies"], {Network contains #})'
- 'not (any(Country, {# in ["us", "gb", "au", "ca"]}))'
- 'Year < 2000' - 'Year < 2000'
- 'Year < 2021 && Omdb.ImdbRating < 7.5' - 'Year < 2021 && Omdb.ImdbRating < 7.5'
- 'AiredEpisodes > 200' - 'AiredEpisodes > 100'
- 'Year > (Now().Year() + 1)' - 'Year > (Now().Year() + 1)'
- 'any (["WWE", "AEW", "WWF", "NXT", "Live:", "Concert", "Musical", " Edition", "Wrestling"], {Title contains #})' - 'any (["WWE", "AEW", "WWF", "NXT", "Live:", "Concert", "Musical", " Edition", "Wrestling"], {Title contains #})'
- 'len(Genres) == 0' - 'len(Genres) == 0'
@@ -158,7 +195,8 @@ pvrs:
root_folder: /mnt/unionfs/Media/Movies root_folder: /mnt/unionfs/Media/Movies
filters: filters:
ignores: 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' - 'Runtime < 60'
- 'len(Genres) == 0' - 'len(Genres) == 0'
- '("music" in Genres || "documentary" in Genres)' - '("music" in Genres || "documentary" in Genres)'

View File

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

71
pvr.go
View File

@@ -1,19 +1,70 @@
package nabarr package nabarr
import "time" import (
"time"
)
/* pvr config / filters */
type PvrConfig struct { type PvrConfig struct {
Name string `yaml:"name"` Name string `yaml:"name"`
Type string `yaml:"type"` Type string `yaml:"type"`
URL string `yaml:"url"` URL string `yaml:"url"`
ApiKey string `yaml:"api_key"` ApiKey string `yaml:"api_key"`
QualityProfile string `yaml:"quality_profile"` QualityProfile string `yaml:"quality_profile"`
RootFolder string `yaml:"root_folder"` RootFolder string `yaml:"root_folder"`
Filters PvrFilters `yaml:"filters"` Options struct {
CacheDuration time.Duration `yaml:"cache_duration"` // add options
Verbosity string `yaml:"verbosity,omitempty"` 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 { type PvrFilters struct {
Ignores []string 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" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/l3uddz/nabarr"
"github.com/l3uddz/nabarr/media" "github.com/l3uddz/nabarr/media"
"github.com/l3uddz/nabarr/util" "github.com/l3uddz/nabarr/util"
"github.com/lucperkins/rek" "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) 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 // prepare request
req := addRequest{ req := addRequest{
Title: item.Title, Title: item.Title,
@@ -130,11 +137,11 @@ func (c *Client) AddMediaItem(item *media.Item) error {
Year: item.Year, Year: item.Year,
QualityProfileId: c.qualityProfileId, QualityProfileId: c.qualityProfileId,
Images: []string{}, Images: []string{},
Monitored: true, Monitored: o.AddMonitored,
RootFolderPath: c.rootFolder, RootFolderPath: c.rootFolder,
MinimumAvailability: "released", MinimumAvailability: "released",
AddOptions: addOptions{ AddOptions: addOptions{
SearchForMovie: true, SearchForMovie: o.SearchMissing,
IgnoreEpisodesWithFiles: false, IgnoreEpisodesWithFiles: false,
IgnoreEpisodesWithoutFiles: false, IgnoreEpisodesWithoutFiles: false,
}, },

View File

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

View File

@@ -20,6 +20,10 @@ type Client struct {
rootFolder string rootFolder string
qualityProfileId int qualityProfileId int
// options
searchMissing bool
addMonitored bool
apiURL string apiURL string
apiHeaders map[string]string apiHeaders map[string]string
apiTimeout time.Duration 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), name: strings.ToLower(c.Name),
testMode: strings.EqualFold(mode, "test"), 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, cache: cc,
cacheTempDuration: c.CacheDuration, cacheTempDuration: c.CacheDuration,

View File

@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/l3uddz/nabarr"
"github.com/l3uddz/nabarr/media" "github.com/l3uddz/nabarr/media"
"github.com/l3uddz/nabarr/util" "github.com/l3uddz/nabarr/util"
"github.com/lucperkins/rek" "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) 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 // prepare request
tvdbId, err := strconv.Atoi(item.TvdbId) tvdbId, err := strconv.Atoi(item.TvdbId)
if err != nil { if err != nil {
@@ -119,15 +126,15 @@ func (c *Client) AddMediaItem(item *media.Item) error {
QualityProfileId: c.qualityProfileId, QualityProfileId: c.qualityProfileId,
Images: []string{}, Images: []string{},
Tags: []string{}, Tags: []string{},
Monitored: true, Monitored: o.AddMonitored,
RootFolderPath: c.rootFolder, RootFolderPath: c.rootFolder,
AddOptions: addOptions{ AddOptions: addOptions{
SearchForMissingEpisodes: true, SearchForMissingEpisodes: o.SearchMissing,
IgnoreEpisodesWithFiles: false, IgnoreEpisodesWithFiles: false,
IgnoreEpisodesWithoutFiles: false, IgnoreEpisodesWithoutFiles: false,
}, },
Seasons: []string{}, Seasons: []string{},
SeriesType: "standard", SeriesType: util.StringOrDefault(o.LookupType, "standard"),
SeasonFolder: true, SeasonFolder: true,
TvdbId: tvdbId, TvdbId: tvdbId,
} }

View File

@@ -3,8 +3,11 @@ package sonarr
import ( import (
"errors" "errors"
"fmt" "fmt"
"github.com/l3uddz/nabarr"
"github.com/l3uddz/nabarr/media" "github.com/l3uddz/nabarr/media"
"github.com/l3uddz/nabarr/util"
"github.com/lefelys/state" "github.com/lefelys/state"
"strings"
) )
func (c *Client) QueueFeedItem(item *media.FeedItem) { 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_tvdb_id", feedItem.TvdbId).
Str("feed_name", feedItem.Feed). Str("feed_name", feedItem.Feed).
Msg("Failed finding item via pvr lookup") Msg("Failed finding item via pvr lookup")
continue
} }
if s.Id > 0 { if s.Id > 0 {
@@ -156,6 +160,23 @@ func (c *Client) queueProcessor(tail state.ShutdownTail) {
continue 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 // add item to pvr
c.log.Debug(). c.log.Debug().
Str("feed_title", mediaItem.FeedTitle). Str("feed_title", mediaItem.FeedTitle).
@@ -172,7 +193,6 @@ func (c *Client) queueProcessor(tail state.ShutdownTail) {
if c.testMode { if c.testMode {
c.log.Info(). c.log.Info().
Err(err).
Str("trakt_title", mediaItem.Title). Str("trakt_title", mediaItem.Title).
Str("trakt_tvdb_id", mediaItem.TvdbId). Str("trakt_tvdb_id", mediaItem.TvdbId).
Int("trakt_year", mediaItem.Year). Int("trakt_year", mediaItem.Year).
@@ -181,7 +201,13 @@ func (c *Client) queueProcessor(tail state.ShutdownTail) {
continue 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(). c.log.Error().
Err(err). Err(err).
Str("feed_title", mediaItem.FeedTitle). Str("feed_title", mediaItem.FeedTitle).
@@ -190,6 +216,7 @@ func (c *Client) queueProcessor(tail state.ShutdownTail) {
Int("trakt_year", mediaItem.Year). Int("trakt_year", mediaItem.Year).
Str("feed_name", feedItem.Feed). Str("feed_name", feedItem.Feed).
Msg("Failed adding item to pvr") Msg("Failed adding item to pvr")
continue
} }
// add item to perm cache (item was added to pvr) // add item to perm cache (item was added to pvr)
@@ -202,7 +229,6 @@ func (c *Client) queueProcessor(tail state.ShutdownTail) {
} }
c.log.Info(). c.log.Info().
Err(err).
Str("trakt_title", mediaItem.Title). Str("trakt_title", mediaItem.Title).
Str("trakt_tvdb_id", mediaItem.TvdbId). Str("trakt_tvdb_id", mediaItem.TvdbId).
Int("trakt_year", mediaItem.Year). Int("trakt_year", mediaItem.Year).

View File

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

View File

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

View File

@@ -20,3 +20,18 @@ func Atof64(val string, defaultVal float64) float64 {
} }
return n 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)
}
})
}
}