From 9c9bb511ea95b07a9bf35bdb5893c477c251093c Mon Sep 17 00:00:00 2001 From: Marcin Federowicz Date: Sat, 31 Oct 2026 02:17:56 +0100 Subject: [PATCH] media filters: add the parental guide filters The contract (trakt-api 719eef7) adds parental_nudity|violence|profanity|alcohol|frightening (range 0-3) and parental_include_unrated to the media filter params. The live API applies them (checked 2026-10-31 on movies/shows popular). Every module with the media filters gets the flags, reusing str.ParentalGuide and the range check of smart_lists -a items. --- CHANGELOG.md | 3 ++ cmds/command.go | 2 + cmds/command_movies.go | 1 + cmds/command_movies_filters_test.go | 2 + cmds/command_shows.go | 1 + cmds/command_shows_filters_test.go | 5 ++- cmds/media_filter_flags.go | 40 ++++++++++++++++++- cmds/media_filter_flags_test.go | 8 ++-- docs/calendars.md | 1 + docs/lists.md | 1 + docs/media.md | 1 + docs/movies.md | 1 + docs/recommendations.md | 1 + docs/shows.md | 1 + docs/users.md | 1 + handlers/commons_helpers.go | 30 +++++++++++++- handlers/movies_media_filters_test.go | 7 +++- handlers/shows_media_filters_test.go | 5 ++- handlers/smart_lists_items_handler.go | 16 +------- handlers/users_activities_handler.go | 36 ++++++++++------- .../users_reviews_activity_handlers_test.go | 8 +++- uri/media_filters.go | 23 +++++++---- uri/social_activity_options.go | 19 ++++++--- uri/uri_test.go | 4 +- 24 files changed, 162 insertions(+), 55 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e41c6e7..6ceafec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,6 +32,9 @@ schedule. ### Added +- Parental guide filters on every list that takes the media filters: `-parental_nudity`, `-parental_violence`, `-parental_profanity`, `-parental_alcohol` and `-parental_frightening`, each a severity range `min-max` from 0 (none) to 3 (severe) such as `-parental_violence 0-1`, and `-parental_include_unrated` to keep titles without a parental guide. They work on the `movies` and `shows` lists (`trending`, `popular`, `anticipated`, ...), `calendars`, `lists`, `media`, `recommendations` and `users -a watchlist|history|collection|list_items|activities`, as on `smart_lists -a items` before. A range outside 0-3 stops the run before any request. The live API applied all five on `movies` and `shows -a popular` (checked 2026-10-31). +- Library: `uri.MediaFilters` and `uri.SocialActivityOptions` have the fields `ParentalAlcohol`, `ParentalFrightening`, `ParentalIncludeUnrated`, `ParentalNudity`, `ParentalProfanity` and `ParentalViolence`. + ### Changed - `younify`: a 403 from Trakt now ends with `younify is not open to API apps yet`, like a 401 did; on 2026-10-31 Trakt answered diff --git a/cmds/command.go b/cmds/command.go index 3a2d4f2..bd9d418 100644 --- a/cmds/command.go +++ b/cmds/command.go @@ -1313,6 +1313,7 @@ func UpdateOptionsWithCommandMoviesFlags(c *Command, options *str.Options) *str. options.ImdbRatings = *_moviesImdbRatings options.RtMeters = *_moviesRtMeters options.RtUserMeters = *_moviesRtUserMeters + *options = _moviesParental.apply(*options) options.Languages = *_languages // the list actions send -start_date / -end_date as media filters; updates reads -start_date through options.StartDate options.MediaStartDate = *_moviesStartDate @@ -1388,6 +1389,7 @@ func UpdateOptionsWithCommandShowsFlags(c *Command, options *str.Options) *str.O options.ImdbRatings = *_showsImdbRatings options.RtMeters = *_showsRtMeters options.RtUserMeters = *_showsRtUserMeters + *options = _showsParental.apply(*options) options.Languages = *_languages // the list actions send -start_date / -end_date as media filters; updates reads -start_date through options.StartDate options.MediaStartDate = *_showsStartDate diff --git a/cmds/command_movies.go b/cmds/command_movies.go index dcb40e0..0e20ca3 100644 --- a/cmds/command_movies.go +++ b/cmds/command_movies.go @@ -31,6 +31,7 @@ var ( _moviesRtMeters = MoviesCmd.Flag.String("rt_meters", consts.EmptyString, consts.RtMetersUsage) _moviesRtUserMeters = MoviesCmd.Flag.String("rt_user_meters", consts.EmptyString, consts.RtUserMetersUsage) _moviesStatus = MoviesCmd.Flag.String("status", consts.EmptyString, consts.MoviesStatusUsage) + _moviesParental = newParentalFilterFlags(&MoviesCmd.Flag) validMoviesActions = []string{ consts.Trending, consts.Popular, consts.Favorited, consts.Played, consts.Watched, consts.Collected, diff --git a/cmds/command_movies_filters_test.go b/cmds/command_movies_filters_test.go index 7fef5cd..389ce40 100644 --- a/cmds/command_movies_filters_test.go +++ b/cmds/command_movies_filters_test.go @@ -36,6 +36,7 @@ func TestMoviesMediaFilterFlags(t *testing.T) { args := []string{"-a", "trending", "-watchnow", "free", "-genres", "action", "-subgenres", "space", "-years", "2020-2026", "-ratings", "75-100", "-runtimes", "90-150", "-countries", "us", "-certifications", "pg-13", "-start_date", "2026-01-01", "-end_date", "2026-12-31", "-imdb_ratings", "8.0-10.0", "-rt_meters", "90-100", "-rt_user_meters", "80-100", "-languages", "en,pl", + "-parental_nudity", "0-1", "-parental_violence", "0-2", "-parental_profanity", "0-1", "-parental_alcohol", "0-3", "-parental_frightening", "1-2", "-parental_include_unrated", "-status", "released,in production"} assert.NoError(t, command.Exec(fs, trakt.NewClient(nil), fileConfig, args)) assert.Equal(t, "trending", got.Action) @@ -52,6 +53,7 @@ func TestMoviesMediaFilterFlags(t *testing.T) { assert.Equal(t, "8.0-10.0", got.ImdbRatings) assert.Equal(t, "90-100", got.RtMeters) assert.Equal(t, "80-100", got.RtUserMeters) + assert.Equal(t, str.ParentalGuide{Nudity: "0-1", Violence: "0-2", Profanity: "0-1", Alcohol: "0-3", Frightening: "1-2", IncludeUnrated: true}, got.Parental) assert.Equal(t, "en,pl", got.Languages) assert.Equal(t, "released,in production", got.MovieStatus) } diff --git a/cmds/command_shows.go b/cmds/command_shows.go index 954646b..7b4f9cc 100644 --- a/cmds/command_shows.go +++ b/cmds/command_shows.go @@ -37,6 +37,7 @@ var ( _showsImdbRatings = ShowsCmd.Flag.String("imdb_ratings", consts.EmptyString, consts.ImdbRatingsUsage) _showsRtMeters = ShowsCmd.Flag.String("rt_meters", consts.EmptyString, consts.RtMetersUsage) _showsRtUserMeters = ShowsCmd.Flag.String("rt_user_meters", consts.EmptyString, consts.RtUserMetersUsage) + _showsParental = newParentalFilterFlags(&ShowsCmd.Flag) validShowsActions = []string{ consts.Trending, consts.Popular, consts.Favorited, consts.Played, consts.Watched, consts.Collected, diff --git a/cmds/command_shows_filters_test.go b/cmds/command_shows_filters_test.go index 1eeece9..9fc49be 100644 --- a/cmds/command_shows_filters_test.go +++ b/cmds/command_shows_filters_test.go @@ -35,7 +35,8 @@ func TestShowsMediaFilterFlags(t *testing.T) { }} args := []string{"-a", "trending", "-watchnow", "free", "-genres", "action", "-subgenres", "space", "-years", "2020-2026", "-ratings", "75-100", "-runtimes", "90-150", "-countries", "us", "-certifications", "tv-14", "-status", "returning series,ended", "-start_date", "2026-01-01", "-end_date", "2026-12-31", - "-languages", "en,pl", "-imdb_ratings", "8.5-10.0", "-rt_meters", "90-100", "-rt_user_meters", "80-100"} + "-languages", "en,pl", "-imdb_ratings", "8.5-10.0", "-rt_meters", "90-100", "-rt_user_meters", "80-100", + "-parental_nudity", "0-1", "-parental_violence", "0-2", "-parental_profanity", "0-1", "-parental_alcohol", "0-3", "-parental_frightening", "1-2", "-parental_include_unrated"} assert.NoError(t, command.Exec(fs, trakt.NewClient(nil), fileConfig, args)) assert.Equal(t, "trending", got.Action) assert.Equal(t, "free", got.WatchNow) @@ -53,12 +54,14 @@ func TestShowsMediaFilterFlags(t *testing.T) { assert.Equal(t, "8.5-10.0", got.ImdbRatings) assert.Equal(t, "90-100", got.RtMeters) assert.Equal(t, "80-100", got.RtUserMeters) + assert.Equal(t, str.ParentalGuide{Nudity: "0-1", Violence: "0-2", Profanity: "0-1", Alcohol: "0-3", Frightening: "1-2", IncludeUnrated: true}, got.Parental) } // TestMediaFilterFlagsAreAvailable checks that movies and shows register every media filter flag that is not global. func TestMediaFilterFlagsAreAvailable(t *testing.T) { filters := []string{"watchnow", "subgenres", "ratings", "certifications", "start_date", "end_date", "genres", "years", "countries", "runtimes", "languages"} filters = append(filters, "imdb_ratings", "rt_meters", "rt_user_meters") + filters = append(filters, "parental_alcohol", "parental_frightening", "parental_include_unrated", "parental_nudity", "parental_profanity", "parental_violence") filters = append(filters, "status") modules := map[*Command][]string{MoviesCmd: filters, ShowsCmd: filters} global := []string{"genres", "years", "countries", "runtimes", "languages"} diff --git a/cmds/media_filter_flags.go b/cmds/media_filter_flags.go index a6e3487..98d651a 100644 --- a/cmds/media_filter_flags.go +++ b/cmds/media_filter_flags.go @@ -17,6 +17,42 @@ type mediaFilterFlags struct { certifications *string startDate *string endDate *string + parental parentalFilterFlags +} + +// parentalFilterFlags are the parental guide filters, for the modules with the media filter flags and for movies and shows. +type parentalFilterFlags struct { + alcohol *string + frightening *string + includeUnrated *bool + nudity *string + profanity *string + violence *string +} + +// newParentalFilterFlags registers the parental guide filter flags in fs. +func newParentalFilterFlags(fs *flag.FlagSet) parentalFilterFlags { + return parentalFilterFlags{ + alcohol: fs.String("parental_alcohol", consts.EmptyString, consts.ParentalRangeUsage), + frightening: fs.String("parental_frightening", consts.EmptyString, consts.ParentalRangeUsage), + includeUnrated: fs.Bool("parental_include_unrated", false, consts.ParentalUnratedUsage), + nudity: fs.String("parental_nudity", consts.EmptyString, consts.ParentalRangeUsage), + profanity: fs.String("parental_profanity", consts.EmptyString, consts.ParentalRangeUsage), + violence: fs.String("parental_violence", consts.EmptyString, consts.ParentalRangeUsage), + } +} + +// apply copies the parental guide filter flags to options. +func (p parentalFilterFlags) apply(options str.Options) str.Options { + options.Parental = str.ParentalGuide{ + Alcohol: *p.alcohol, + Frightening: *p.frightening, + IncludeUnrated: *p.includeUnrated, + Nudity: *p.nudity, + Profanity: *p.profanity, + Violence: *p.violence, + } + return options } // newMediaFilterFlags registers the media filter flags in fs. @@ -28,6 +64,7 @@ func newMediaFilterFlags(fs *flag.FlagSet) mediaFilterFlags { certifications: fs.String("certifications", consts.EmptyString, consts.CertificationsUsage), startDate: fs.String("start_date", consts.EmptyString, consts.FilterStartDateUsage), endDate: fs.String("end_date", consts.EmptyString, consts.EndDateUsage), + parental: newParentalFilterFlags(fs), } } @@ -39,6 +76,7 @@ func newMediaFilterFlagsWithoutDates(fs *flag.FlagSet) mediaFilterFlags { subgenres: fs.String("subgenres", consts.EmptyString, consts.SubgenresUsage), ratings: fs.String("ratings", consts.EmptyString, consts.RatingsFilterUsage), certifications: fs.String("certifications", consts.EmptyString, consts.CertificationsUsage), + parental: newParentalFilterFlags(fs), } } @@ -59,5 +97,5 @@ func (m mediaFilterFlags) apply(options str.Options) str.Options { options.Countries = *_countries options.Runtimes = *_runtimes options.Languages = *_languages - return options + return m.parental.apply(options) } diff --git a/cmds/media_filter_flags_test.go b/cmds/media_filter_flags_test.go index fa110ea..258744b 100644 --- a/cmds/media_filter_flags_test.go +++ b/cmds/media_filter_flags_test.go @@ -49,9 +49,10 @@ func TestModuleMediaFilterFlags(t *testing.T) { return nil }} args := []string{"-a", m.action, "-watchnow", "free", "-genres", "action", "-subgenres", "space", "-years", "2020-2026", "-ratings", "75-100", - "-runtimes", "90-150", "-countries", "us", "-certifications", "pg-13", "-languages", "en,pl"} + "-runtimes", "90-150", "-countries", "us", "-certifications", "pg-13", "-languages", "en,pl", + "-parental_nudity", "0-1", "-parental_violence", "0-2", "-parental_profanity", "0-1", "-parental_alcohol", "0-3", "-parental_frightening", "1-2", "-parental_include_unrated"} want := str.Options{WatchNow: "free", Genres: "action", Subgenres: "space", Years: "2020-2026", Ratings: "75-100", Runtimes: "90-150", - Countries: "us", Certifications: "pg-13", Languages: "en,pl"} + Countries: "us", Certifications: "pg-13", Languages: "en,pl", Parental: str.ParentalGuide{Nudity: "0-1", Violence: "0-2", Profanity: "0-1", Alcohol: "0-3", Frightening: "1-2", IncludeUnrated: true}} if m.dates { args = append(args, "-start_date", "2026-01-01", "-end_date", "2026-12-31") want.MediaStartDate = "2026-01-01" @@ -60,7 +61,8 @@ func TestModuleMediaFilterFlags(t *testing.T) { assert.NoError(t, command.Exec(fs, trakt.NewClient(nil), fileConfig, args)) assert.Equal(t, m.action, got.Action) assert.Equal(t, want, str.Options{WatchNow: got.WatchNow, Genres: got.Genres, Subgenres: got.Subgenres, Years: got.Years, Ratings: got.Ratings, - Runtimes: got.Runtimes, Countries: got.Countries, Certifications: got.Certifications, MediaStartDate: got.MediaStartDate, MediaEndDate: got.MediaEndDate, Languages: got.Languages}) + Runtimes: got.Runtimes, Countries: got.Countries, Certifications: got.Certifications, MediaStartDate: got.MediaStartDate, MediaEndDate: got.MediaEndDate, Languages: got.Languages, + Parental: got.Parental}) }) } } diff --git a/docs/calendars.md b/docs/calendars.md index 2e4203a..0a8d7ff 100644 --- a/docs/calendars.md +++ b/docs/calendars.md @@ -41,6 +41,7 @@ Every calendar action takes the Trakt media filters: `-genres`, `-subgenres`, `- `-certifications` (comma separated where a filter takes several values) and `-watchnow favorites|any|any_all|free|free_all|subscriptions|subscriptions_all`. `-languages` (language codes, comma separated) filters them too. +`-parental_nudity`, `-parental_violence`, `-parental_profanity`, `-parental_alcohol` and `-parental_frightening` filter by parental guide: each takes a severity range `min-max` from 0 (none) to 3 (severe), ie: `-parental_violence 0-1`. Titles without a parental guide are left out, unless you add `-parental_include_unrated`. The dates of a calendar are set with `-start_date` and `-days`. ```console $ ./trakt-sync calendars -a all_movies -genres horror -countries us diff --git a/docs/lists.md b/docs/lists.md index 184af37..d1f1b9e 100644 --- a/docs/lists.md +++ b/docs/lists.md @@ -50,6 +50,7 @@ $ ./trakt-sync lists -a report -trakt_id 2142753 -r spam -message "only ads" `-certifications` (comma separated where a filter takes several values), `-start_date`, `-end_date` and `-watchnow favorites|any|any_all|free|free_all|subscriptions|subscriptions_all`. `-languages` (language codes, comma separated) filters `items`; on 2026-10-03 the live API ignored it for `trending` and `popular`. +`-parental_nudity`, `-parental_violence`, `-parental_profanity`, `-parental_alcohol` and `-parental_frightening` filter by parental guide: each takes a severity range `min-max` from 0 (none) to 3 (severe), ie: `-parental_violence 0-1`. Titles without a parental guide are left out, unless you add `-parental_include_unrated`. ```console $ ./trakt-sync lists -a items -trakt_id 2142753 -genres horror -years 2020-2026 ``` diff --git a/docs/media.md b/docs/media.md index ec6f7f8..3532e9a 100644 --- a/docs/media.md +++ b/docs/media.md @@ -13,6 +13,7 @@ All three actions take the Trakt media filters: `-genres`, `-subgenres`, `-years `-certifications` (comma separated where a filter takes several values), `-start_date`, `-end_date` and `-watchnow favorites|any|any_all|free|free_all|subscriptions|subscriptions_all`. `-languages` (language codes, comma separated) filters them too. +`-parental_nudity`, `-parental_violence`, `-parental_profanity`, `-parental_alcohol` and `-parental_frightening` filter by parental guide: each takes a severity range `min-max` from 0 (none) to 3 (severe), ie: `-parental_violence 0-1`. Titles without a parental guide are left out, unless you add `-parental_include_unrated`. `-theme ` applies a seasonal theme such as `halloween` or `christmas`: Trakt turns it into filters for the list, a filter you set yourself wins over it, and an unknown theme is ignored. ```console $ ./trakt-sync media -a trending -theme halloween diff --git a/docs/movies.md b/docs/movies.md index de2d398..81c2225 100644 --- a/docs/movies.md +++ b/docs/movies.md @@ -78,6 +78,7 @@ filter takes several values), `-start_date`, `-end_date`, `-status` (`released`, `in production`, `post production`, `planned`, `rumored`, `canceled`; comma separated). They also take `-languages` (language codes, comma separated), `-imdb_ratings` (range, for example `8.0-10.0`), `-rt_meters` and `-rt_user_meters` (Rotten Tomatoes ranges, for example `90-100`). +`-parental_nudity`, `-parental_violence`, `-parental_profanity`, `-parental_alcohol` and `-parental_frightening` filter by parental guide: each takes a severity range `min-max` from 0 (none) to 3 (severe), ie: `-parental_violence 0-1`. Titles without a parental guide are left out, unless you add `-parental_include_unrated`. The live API applied all five on `movies -a popular` (checked 2026-10-31). `trending`, `popular` and `anticipated` also take `-theme `, a seasonal theme such as `halloween` or `christmas`: Trakt turns it into filters for the list, a filter you set yourself wins over it, and an unknown theme is ignored. ```console $ ./trakt-sync movies -a popular -theme halloween diff --git a/docs/recommendations.md b/docs/recommendations.md index 4d770f4..0bafbbc 100644 --- a/docs/recommendations.md +++ b/docs/recommendations.md @@ -38,6 +38,7 @@ $ ./trakt-sync recommendations -a movies -ex full `-certifications` (comma separated where a filter takes several values), `-start_date`, `-end_date` and `-watchnow favorites|any|any_all|free|free_all|subscriptions|subscriptions_all`. `-languages` (language codes, comma separated) filters them too. +`-parental_nudity`, `-parental_violence`, `-parental_profanity`, `-parental_alcohol` and `-parental_frightening` filter by parental guide: each takes a severity range `min-max` from 0 (none) to 3 (severe), ie: `-parental_violence 0-1`. Titles without a parental guide are left out, unless you add `-parental_include_unrated`. `-theme ` applies a seasonal theme such as `halloween` or `christmas`: Trakt turns it into filters for the recommendations, a filter you set yourself wins over it, and an unknown theme is ignored. ```console $ ./trakt-sync recommendations -a movies -genres horror -years 2020-2026 diff --git a/docs/shows.md b/docs/shows.md index ea4115e..67e482a 100644 --- a/docs/shows.md +++ b/docs/shows.md @@ -69,6 +69,7 @@ filter takes several values), `-start_date`, `-end_date`, `-status` (`returning series`, `continuing`, `in production`, `planned`, `upcoming`, `pilot`, `canceled`, `ended`; comma separated). They also take `-languages` (language codes, comma separated), `-imdb_ratings` (range, for example `8.5-10.0`), `-rt_meters` and `-rt_user_meters` (Rotten Tomatoes ranges, for example `90-100`). +`-parental_nudity`, `-parental_violence`, `-parental_profanity`, `-parental_alcohol` and `-parental_frightening` filter by parental guide: each takes a severity range `min-max` from 0 (none) to 3 (severe), ie: `-parental_violence 0-1`. Titles without a parental guide are left out, unless you add `-parental_include_unrated`. The live API applied all five on `shows -a popular` (checked 2026-10-31). `trending`, `popular` and `anticipated` also take `-theme `, a seasonal theme such as `halloween` or `christmas`: Trakt turns it into filters for the list, a filter you set yourself wins over it, and an unknown theme is ignored. ```console $ ./trakt-sync shows -a trending -theme christmas diff --git a/docs/users.md b/docs/users.md index 7a471d5..0fb5fbf 100644 --- a/docs/users.md +++ b/docs/users.md @@ -856,6 +856,7 @@ $ ./trakt-sync users -a plex_sync -i -all_data `watchlist`, `history`, `collection` and `list_items` take the Trakt media filters: `-genres`, `-subgenres`, `-years`, `-ratings`, `-runtimes`, `-countries`, `-certifications` (comma separated where a filter takes several values), `-start_date`, `-end_date`, `-watchnow favorites|any|any_all|free|free_all|subscriptions|subscriptions_all` and `-languages` (language codes, comma separated). +`-parental_nudity`, `-parental_violence`, `-parental_profanity`, `-parental_alcohol` and `-parental_frightening` filter by parental guide: each takes a severity range `min-max` from 0 (none) to 3 (severe), ie: `-parental_violence 0-1`. Titles without a parental guide are left out, unless you add `-parental_include_unrated`. `activities` takes them too. `collection` sends the filters, as the Trakt API contract lists them for this route, but on 2026-10-04 the live API ignored `-genres`, `-years`, `-languages` and `-watchnow` there and returned the whole collection. In `history`, `-start_at` and `-end_at` still set the watched-at window; `-start_date` and `-end_date` are the separate media date filters. diff --git a/handlers/commons_helpers.go b/handlers/commons_helpers.go index f40c3d1..f9afc88 100644 --- a/handlers/commons_helpers.go +++ b/handlers/commons_helpers.go @@ -7,6 +7,7 @@ import ( "errors" "fmt" "net/http" + "regexp" "slices" "strconv" "strings" @@ -163,14 +164,41 @@ func mediaFilters(options *str.Options) uri.MediaFilters { ImdbRatings: options.ImdbRatings, RtMeters: options.RtMeters, RtUserMeters: options.RtUserMeters, + + ParentalAlcohol: options.Parental.Alcohol, + ParentalFrightening: options.Parental.Frightening, + ParentalIncludeUnrated: options.Parental.IncludeUnrated, + ParentalNudity: options.Parental.Nudity, + ParentalProfanity: options.Parental.Profanity, + ParentalViolence: options.Parental.Violence, } } -// checkMediaFilters reports a -watchnow value the API does not know. +// checkMediaFilters reports a -watchnow value the API does not know, or a parental guide range that is not min-max from 0 to 3. func checkMediaFilters(options *str.Options) error { if len(options.WatchNow) > consts.ZeroValue && !cfg.IsValidConfigType(cfg.WatchNowFilters, options.WatchNow) { return fmt.Errorf("watchnow '%s' is not valid, available values: %v", options.WatchNow, cfg.WatchNowFilters) } + return checkParentalRanges(options.Parental) +} + +// parentalRange is a parental guide severity range min-max, from 0 (none) to 3 (severe). +var parentalRange = regexp.MustCompile(`^[0-3]-[0-3]$`) + +// checkParentalRanges reports a parental guide range that is not min-max from 0 to 3. +func checkParentalRanges(p str.ParentalGuide) error { + ranges := []struct{ name, value string }{ + {"parental_alcohol", p.Alcohol}, + {"parental_frightening", p.Frightening}, + {"parental_nudity", p.Nudity}, + {"parental_profanity", p.Profanity}, + {"parental_violence", p.Violence}, + } + for _, r := range ranges { + if len(r.value) > consts.ZeroValue && !parentalRange.MatchString(r.value) { + return fmt.Errorf("%s '%s' is not valid, want a range min-max from 0 to 3 ie: 0-1", r.name, r.value) + } + } return nil } diff --git a/handlers/movies_media_filters_test.go b/handlers/movies_media_filters_test.go index 689426f..efcd200 100644 --- a/handlers/movies_media_filters_test.go +++ b/handlers/movies_media_filters_test.go @@ -13,10 +13,12 @@ import ( // the movies list actions send the media filter flags and -status as query parameters. func TestMoviesHandlersMediaFilters(t *testing.T) { - const filtered = "certifications=pg-13&countries=us&end_date=2026-12-31&genres=action%2Cdrama&imdb_ratings=8.0-10.0&languages=en%2Cpl&page=1&ratings=75-100" + + const filtered = "certifications=pg-13&countries=us&end_date=2026-12-31&genres=action%2Cdrama&imdb_ratings=8.0-10.0&languages=en%2Cpl&page=1" + + "&parental_alcohol=0-3&parental_frightening=1-2&parental_include_unrated=true&parental_nudity=0-1&parental_profanity=0-1&parental_violence=0-2&ratings=75-100" + "&rt_meters=90-100&rt_user_meters=80-100&runtimes=90-150&start_date=2026-01-01&statuses=released%2Cin+production&subgenres=space&watchnow=free&years=2020-2026" filters := str.Options{WatchNow: "free", Genres: "action,drama", Subgenres: "space", Years: "2020-2026", Ratings: "75-100", Runtimes: "90-150", Countries: "us", Certifications: "pg-13", MediaStartDate: "2026-01-01", MediaEndDate: "2026-12-31", - ImdbRatings: "8.0-10.0", RtMeters: "90-100", RtUserMeters: "80-100", Languages: "en,pl", MovieStatus: "released,in production"} + ImdbRatings: "8.0-10.0", RtMeters: "90-100", RtUserMeters: "80-100", Languages: "en,pl", MovieStatus: "released,in production", + Parental: str.ParentalGuide{Nudity: "0-1", Violence: "0-2", Profanity: "0-1", Alcohol: "0-3", Frightening: "1-2", IncludeUnrated: true}} handlers := []struct { name string handler Handler @@ -43,6 +45,7 @@ func TestMoviesHandlersMediaFilters(t *testing.T) { {name: "unknown watchnow", options: str.Options{WatchNow: "cinema"}, wantErr: "watchnow 'cinema' is not valid"}, {name: "one status", options: str.Options{MovieStatus: "rumored"}, query: "page=1&statuses=rumored"}, {name: "unknown status", options: str.Options{MovieStatus: "released,ended"}, wantErr: "status 'ended' is not valid"}, + {name: "parental range not min-max", options: str.Options{Parental: str.ParentalGuide{Violence: "2"}}, wantErr: "parental_violence '2' is not valid"}, } for _, h := range handlers { diff --git a/handlers/shows_media_filters_test.go b/handlers/shows_media_filters_test.go index 529c8ba..36c0c8d 100644 --- a/handlers/shows_media_filters_test.go +++ b/handlers/shows_media_filters_test.go @@ -13,13 +13,15 @@ import ( // the shows list actions send the media filter flags and -status as query parameters. func TestShowsHandlersMediaFilters(t *testing.T) { - const filtered = "certifications=tv-14&countries=us&end_date=2026-12-31&genres=action%2Cdrama&imdb_ratings=8.5-10.0&languages=en%2Cpl&page=1&ratings=75-100" + + const filtered = "certifications=tv-14&countries=us&end_date=2026-12-31&genres=action%2Cdrama&imdb_ratings=8.5-10.0&languages=en%2Cpl&page=1" + + "&parental_alcohol=0-3&parental_frightening=1-2&parental_include_unrated=true&parental_nudity=0-1&parental_profanity=0-1&parental_violence=0-2&ratings=75-100" + "&rt_meters=90-100&rt_user_meters=80-100&runtimes=30-60" + "&start_date=2026-01-01&statuses=returning+series%2Cended&subgenres=space&watchnow=free&years=2020-2026" filters := str.Options{ WatchNow: "free", Genres: "action,drama", Subgenres: "space", Years: "2020-2026", Ratings: "75-100", Runtimes: "30-60", Countries: "us", Certifications: "tv-14", MediaStartDate: "2026-01-01", MediaEndDate: "2026-12-31", ShowStatus: "returning series,ended", Languages: "en,pl", ImdbRatings: "8.5-10.0", RtMeters: "90-100", RtUserMeters: "80-100", + Parental: str.ParentalGuide{Nudity: "0-1", Violence: "0-2", Profanity: "0-1", Alcohol: "0-3", Frightening: "1-2", IncludeUnrated: true}, } handlers := []struct { name string @@ -44,6 +46,7 @@ func TestShowsHandlersMediaFilters(t *testing.T) { {name: "no filters", query: "page=1"}, {name: "unknown watchnow", options: str.Options{WatchNow: "cinema"}, wantErr: "watchnow 'cinema' is not valid"}, {name: "unknown status", options: str.Options{ShowStatus: "ended,running"}, wantErr: "status 'running' is not valid"}, + {name: "parental range out of 0-3", options: str.Options{Parental: str.ParentalGuide{Nudity: "0-4"}}, wantErr: "parental_nudity '0-4' is not valid"}, } for _, h := range handlers { diff --git a/handlers/smart_lists_items_handler.go b/handlers/smart_lists_items_handler.go index 412baed..25e61aa 100644 --- a/handlers/smart_lists_items_handler.go +++ b/handlers/smart_lists_items_handler.go @@ -15,9 +15,6 @@ import ( "github.com/mfederowicz/trakt-sync/uri" ) -// parentalRange is a parental guide severity range min-max, from 0 (none) to 3 (severe). -var parentalRange = regexp.MustCompile(`^[0-3]-[0-3]$`) - // watchNowCountry is the two-letter lowercase region of the watchnow filter. var watchNowCountry = regexp.MustCompile(`^[a-z]{2}$`) @@ -144,17 +141,8 @@ func validSmartListItemsFilters(options *str.Options) error { return fmt.Errorf("watchnow_country '%s' is not valid, want a 2 character lowercase code ie: us", options.WatchNowCountry) } - ranges := []struct{ name, value string }{ - {"parental_alcohol", options.Parental.Alcohol}, - {"parental_frightening", options.Parental.Frightening}, - {"parental_nudity", options.Parental.Nudity}, - {"parental_profanity", options.Parental.Profanity}, - {"parental_violence", options.Parental.Violence}, - } - for _, r := range ranges { - if len(r.value) > consts.ZeroValue && !parentalRange.MatchString(r.value) { - return fmt.Errorf("%s '%s' is not valid, want a range min-max from 0 to 3 ie: 0-1", r.name, r.value) - } + if err := checkParentalRanges(options.Parental); err != nil { + return err } // 0 is the flag not given diff --git a/handlers/users_activities_handler.go b/handlers/users_activities_handler.go index 6b37439..0137306 100644 --- a/handlers/users_activities_handler.go +++ b/handlers/users_activities_handler.go @@ -30,21 +30,27 @@ func (h UsersActivitiesHandler) Handle(options *str.Options, client *trakt.Clien printer.Println("Returns recent activity of your " + options.Type) opts := uri.SocialActivityOptions{ - Limit: options.PerPage, - Extended: options.ExtendedInfo, - WatchNow: options.WatchNow, - Genres: options.Genres, - Subgenres: options.Subgenres, - Years: options.Years, - Ratings: options.Ratings, - StartDate: options.MediaStartDate, - EndDate: options.MediaEndDate, - Runtimes: options.Runtimes, - Countries: options.Countries, - Certifications: options.Certifications, - IgnoreCollected: options.IgnoreCollected, - IgnoreWatched: options.IgnoreWatched, - IgnoreWatchlisted: options.IgnoreWatchlisted, + Limit: options.PerPage, + Extended: options.ExtendedInfo, + WatchNow: options.WatchNow, + Genres: options.Genres, + Subgenres: options.Subgenres, + Years: options.Years, + Ratings: options.Ratings, + StartDate: options.MediaStartDate, + EndDate: options.MediaEndDate, + Runtimes: options.Runtimes, + Countries: options.Countries, + Certifications: options.Certifications, + ParentalAlcohol: options.Parental.Alcohol, + ParentalFrightening: options.Parental.Frightening, + ParentalIncludeUnrated: options.Parental.IncludeUnrated, + ParentalNudity: options.Parental.Nudity, + ParentalProfanity: options.Parental.Profanity, + ParentalViolence: options.Parental.Violence, + IgnoreCollected: options.IgnoreCollected, + IgnoreWatched: options.IgnoreWatched, + IgnoreWatchlisted: options.IgnoreWatchlisted, } result, err := h.fetchActivities(client, options, &opts, consts.DefaultPage) if apiErr := notOpenToAPIApps(options.Action, err); apiErr != nil { diff --git a/handlers/users_reviews_activity_handlers_test.go b/handlers/users_reviews_activity_handlers_test.go index 9e56bba..aaec9c0 100644 --- a/handlers/users_reviews_activity_handlers_test.go +++ b/handlers/users_reviews_activity_handlers_test.go @@ -34,11 +34,15 @@ func TestUsersReviewsAndActivityHandlers(t *testing.T) { path: "/users/sean/friends/activities", query: "genres=drama&limit=10&page=1", status: http.StatusOK, body: `[{"id":1,"type":"movie","movie":{"title":"Arrival"}}]`}, {name: "activities all filters", handler: UsersActivitiesHandler{}, options: str.Options{Action: consts.Activities, Type: "following", WatchNow: "free", Genres: "action,drama", Subgenres: "space", Years: "2020-2026", Ratings: "75-100", Runtimes: "90-150", Countries: "us", Certifications: "pg-13", MediaStartDate: "2026-01-01", MediaEndDate: "2026-12-31", - IgnoreCollected: "true", IgnoreWatched: "true", IgnoreWatchlisted: "false"}, + IgnoreCollected: "true", IgnoreWatched: "true", IgnoreWatchlisted: "false", + Parental: str.ParentalGuide{Nudity: "0-1", Violence: "0-2", Profanity: "0-1", Alcohol: "0-3", Frightening: "1-2", IncludeUnrated: true}}, path: "/users/sean/following/activities", query: "certifications=pg-13&countries=us&end_date=2026-12-31&genres=action%2Cdrama&ignore_collected=true&ignore_watched=true&ignore_watchlisted=false" + - "&page=1&ratings=75-100&runtimes=90-150&start_date=2026-01-01&subgenres=space&watchnow=free&years=2020-2026", + "&page=1&parental_alcohol=0-3&parental_frightening=1-2&parental_include_unrated=true&parental_nudity=0-1&parental_profanity=0-1&parental_violence=0-2" + + "&ratings=75-100&runtimes=90-150&start_date=2026-01-01&subgenres=space&watchnow=free&years=2020-2026", status: http.StatusOK, body: `[{"id":1,"type":"movie","movie":{"title":"Arrival"}}]`}, + {name: "activities parental range out of 0-3", handler: UsersActivitiesHandler{}, options: str.Options{Action: consts.Activities, Type: "friends", + Parental: str.ParentalGuide{Alcohol: "1-5"}}, wantErr: "parental_alcohol '1-5' is not valid", wantNoAPI: true}, {name: "activities unknown watchnow", handler: UsersActivitiesHandler{}, options: str.Options{Action: consts.Activities, Type: "friends", WatchNow: "cinema"}, wantErr: "watchnow 'cinema' is not valid", wantNoAPI: true}, {name: "activities without type", handler: UsersActivitiesHandler{}, options: str.Options{Action: consts.Activities}, diff --git a/uri/media_filters.go b/uri/media_filters.go index 039efcd..a5146e1 100644 --- a/uri/media_filters.go +++ b/uri/media_filters.go @@ -9,12 +9,19 @@ type MediaFilters struct { Genres string `url:"genres,omitempty"` ImdbRatings string `url:"imdb_ratings,omitempty"` Languages string `url:"languages,omitempty"` - Ratings string `url:"ratings,omitempty"` - RtMeters string `url:"rt_meters,omitempty"` - RtUserMeters string `url:"rt_user_meters,omitempty"` - Runtimes string `url:"runtimes,omitempty"` - StartDate string `url:"start_date,omitempty"` - Subgenres string `url:"subgenres,omitempty"` - WatchNow string `url:"watchnow,omitempty"` - Years string `url:"years,omitempty"` + // ParentalAlcohol and the other parental guide filters take a severity range from 0 (none) to 3 (severe), ie: 0-1 + ParentalAlcohol string `url:"parental_alcohol,omitempty"` + ParentalFrightening string `url:"parental_frightening,omitempty"` + ParentalIncludeUnrated bool `url:"parental_include_unrated,omitempty"` + ParentalNudity string `url:"parental_nudity,omitempty"` + ParentalProfanity string `url:"parental_profanity,omitempty"` + ParentalViolence string `url:"parental_violence,omitempty"` + Ratings string `url:"ratings,omitempty"` + RtMeters string `url:"rt_meters,omitempty"` + RtUserMeters string `url:"rt_user_meters,omitempty"` + Runtimes string `url:"runtimes,omitempty"` + StartDate string `url:"start_date,omitempty"` + Subgenres string `url:"subgenres,omitempty"` + WatchNow string `url:"watchnow,omitempty"` + Years string `url:"years,omitempty"` } diff --git a/uri/social_activity_options.go b/uri/social_activity_options.go index 148c887..c139763 100644 --- a/uri/social_activity_options.go +++ b/uri/social_activity_options.go @@ -12,10 +12,17 @@ type SocialActivityOptions struct { IgnoreWatchlisted string `url:"ignore_watchlisted,omitempty"` Limit int `url:"limit,omitempty"` Page int `url:"page,omitempty"` - Ratings string `url:"ratings,omitempty"` - Runtimes string `url:"runtimes,omitempty"` - StartDate string `url:"start_date,omitempty"` - Subgenres string `url:"subgenres,omitempty"` - WatchNow string `url:"watchnow,omitempty"` - Years string `url:"years,omitempty"` + // ParentalAlcohol and the other parental guide filters take a severity range from 0 (none) to 3 (severe), ie: 0-1 + ParentalAlcohol string `url:"parental_alcohol,omitempty"` + ParentalFrightening string `url:"parental_frightening,omitempty"` + ParentalIncludeUnrated bool `url:"parental_include_unrated,omitempty"` + ParentalNudity string `url:"parental_nudity,omitempty"` + ParentalProfanity string `url:"parental_profanity,omitempty"` + ParentalViolence string `url:"parental_violence,omitempty"` + Ratings string `url:"ratings,omitempty"` + Runtimes string `url:"runtimes,omitempty"` + StartDate string `url:"start_date,omitempty"` + Subgenres string `url:"subgenres,omitempty"` + WatchNow string `url:"watchnow,omitempty"` + Years string `url:"years,omitempty"` } diff --git a/uri/uri_test.go b/uri/uri_test.go index 965a998..502c6ca 100644 --- a/uri/uri_test.go +++ b/uri/uri_test.go @@ -149,9 +149,11 @@ func TestBuildQueryMediaFilters(t *testing.T) { WatchNow: "free", Genres: "action,drama", Subgenres: "space", Years: "2020-2026", Ratings: "75-100", StartDate: "2026-01-01", EndDate: "2026-12-31", Runtimes: "90-150", Countries: "us", Certifications: "pg-13", ImdbRatings: "8.0-10.0", Languages: "en,pl", RtMeters: "90-100", RtUserMeters: "80-100", + ParentalAlcohol: "0-3", ParentalFrightening: "1-2", ParentalIncludeUnrated: true, ParentalNudity: "0-1", ParentalProfanity: "0-1", ParentalViolence: "0-2", }} expectedURL := BaseURL + "?certifications=pg-13&countries=us&end_date=2026-12-31&genres=" + url.QueryEscape("action,drama") + - "&imdb_ratings=8.0-10.0&languages=" + url.QueryEscape("en,pl") + "&page=2&ratings=75-100&rt_meters=90-100&rt_user_meters=80-100" + + "&imdb_ratings=8.0-10.0&languages=" + url.QueryEscape("en,pl") + "&page=2" + + "&parental_alcohol=0-3&parental_frightening=1-2&parental_include_unrated=true&parental_nudity=0-1&parental_profanity=0-1&parental_violence=0-2&ratings=75-100&rt_meters=90-100&rt_user_meters=80-100" + "&runtimes=90-150&start_date=2026-01-01&subgenres=space&watchnow=free&years=2020-2026" got, err := AddQuery(BaseURL, opts) if err != nil {