Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions cmds/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions cmds/command_movies.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions cmds/command_movies_filters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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)
}
1 change: 1 addition & 0 deletions cmds/command_shows.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 4 additions & 1 deletion cmds/command_shows_filters_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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"}
Expand Down
40 changes: 39 additions & 1 deletion cmds/media_filter_flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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),
}
}

Expand All @@ -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),
}
}

Expand All @@ -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)
}
8 changes: 5 additions & 3 deletions cmds/media_filter_flags_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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})
})
}
}
1 change: 1 addition & 0 deletions docs/calendars.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions docs/lists.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
1 change: 1 addition & 0 deletions docs/media.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>` 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
Expand Down
1 change: 1 addition & 0 deletions docs/movies.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>`, 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
Expand Down
1 change: 1 addition & 0 deletions docs/recommendations.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>` 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
Expand Down
1 change: 1 addition & 0 deletions docs/shows.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <name>`, 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
Expand Down
1 change: 1 addition & 0 deletions docs/users.md
Original file line number Diff line number Diff line change
Expand Up @@ -856,6 +856,7 @@ $ ./trakt-sync users -a plex_sync -i <server id> -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.
Expand Down
30 changes: 29 additions & 1 deletion handlers/commons_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"errors"
"fmt"
"net/http"
"regexp"
"slices"
"strconv"
"strings"
Expand Down Expand Up @@ -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
}

Expand Down
Loading