diff --git a/README.md b/README.md index 0c7a421..ce785cf 100644 --- a/README.md +++ b/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)' diff --git a/cmd/nabarr/pvr/pvr.go b/cmd/nabarr/pvr/pvr.go index 523abd9..675ff1a 100644 --- a/cmd/nabarr/pvr/pvr.go +++ b/cmd/nabarr/pvr/pvr.go @@ -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) diff --git a/pvr.go b/pvr.go index 8265cf6..e0f4e30 100644 --- a/pvr.go +++ b/pvr.go @@ -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 + } +} diff --git a/radarr/api.go b/radarr/api.go index f052b72..f7121cc 100644 --- a/radarr/api.go +++ b/radarr/api.go @@ -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, }, diff --git a/radarr/queue.go b/radarr/queue.go index 38296e0..f0b069a 100644 --- a/radarr/queue.go +++ b/radarr/queue.go @@ -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). diff --git a/radarr/radarr.go b/radarr/radarr.go index 6aca6c2..18bf7e4 100644 --- a/radarr/radarr.go +++ b/radarr/radarr.go @@ -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, diff --git a/sonarr/api.go b/sonarr/api.go index 0e58b05..f571f48 100644 --- a/sonarr/api.go +++ b/sonarr/api.go @@ -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, } diff --git a/sonarr/queue.go b/sonarr/queue.go index 08570ea..295a26b 100644 --- a/sonarr/queue.go +++ b/sonarr/queue.go @@ -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). diff --git a/sonarr/sonarr.go b/sonarr/sonarr.go index d2b1aca..10ec895 100644 --- a/sonarr/sonarr.go +++ b/sonarr/sonarr.go @@ -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), diff --git a/sonarr/struct.go b/sonarr/struct.go index 018e314..377cbd2 100644 --- a/sonarr/struct.go +++ b/sonarr/struct.go @@ -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 { diff --git a/util/default.go b/util/default.go index 3ab3834..c3e7350 100644 --- a/util/default.go +++ b/util/default.go @@ -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 +} diff --git a/util/default_test.go b/util/default_test.go index 85c8d2d..b01d5ad 100644 --- a/util/default_test.go +++ b/util/default_test.go @@ -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) + } + }) + } +} diff --git a/util/slice.go b/util/slice.go new file mode 100644 index 0000000..3272f07 --- /dev/null +++ b/util/slice.go @@ -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 +} diff --git a/util/slice_test.go b/util/slice_test.go new file mode 100644 index 0000000..a56d81e --- /dev/null +++ b/util/slice_test.go @@ -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) + } + }) + } +}