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:
48
README.md
48
README.md
@@ -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)'
|
||||
|
||||
@@ -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)
|
||||
|
||||
53
pvr.go
53
pvr.go
@@ -1,6 +1,10 @@
|
||||
package nabarr
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
/* pvr config / filters */
|
||||
|
||||
type PvrConfig struct {
|
||||
Name string `yaml:"name"`
|
||||
@@ -9,6 +13,13 @@ type PvrConfig struct {
|
||||
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"`
|
||||
@@ -17,3 +28,43 @@ type PvrConfig struct {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
@@ -66,6 +70,8 @@ func New(c nabarr.PvrConfig, mode string, m *media.Client, cc *cache.Client) (*C
|
||||
testMode: strings.EqualFold(mode, "test"),
|
||||
|
||||
rootFolder: c.RootFolder,
|
||||
searchMissing: util.BoolOrDefault(c.Options.SearchMissing, true),
|
||||
addMonitored: util.BoolOrDefault(c.Options.AddMonitored, true),
|
||||
|
||||
cache: cc,
|
||||
cacheTempDuration: c.CacheDuration,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
12
util/slice.go
Normal 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
39
util/slice_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user