From c7ccae1253bf0153a3676843a7ada0c87e9ce9b8 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Wed, 15 Jun 2022 23:20:40 -0400 Subject: [PATCH] [41] add video_codec, video_profile, audio_codec, audio_profile, channels, height, width, and aspect filters --- VERSION | 2 +- docs/metadata/filters.md | 34 +++++++++++++++++++++++----------- modules/builder.py | 34 ++++++++++++++++++++++------------ modules/plex.py | 26 +++++++++++++++++++++++--- modules/util.py | 4 +++- 5 files changed, 72 insertions(+), 28 deletions(-) diff --git a/VERSION b/VERSION index 0a19e65a..61420b25 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.17.0-develop40 +1.17.0-develop41 diff --git a/docs/metadata/filters.md b/docs/metadata/filters.md index a5960cf8..341d4825 100644 --- a/docs/metadata/filters.md +++ b/docs/metadata/filters.md @@ -78,6 +78,10 @@ Tag filters can take multiple values as a **list or a comma-separated string**. | `resolution` | Uses the resolution tag to match | ✅ | ✅1 | ✅1 | ✅ | ❌ | ❌ | ❌ | | `audio_language` | Uses the audio language tags to match | ✅ | ✅1 | ✅1 | ✅ | ❌ | ❌ | ❌ | | `subtitle_language` | Uses the subtitle language tags to match | ✅ | ✅1 | ✅1 | ✅ | ❌ | ❌ | ❌ | +| `video_codec` | Uses the video codec tags to match | ✅ | ✅1 | ✅1 | ✅ | ❌ | ❌ | ❌ | +| `video_profile` | Uses the video profile tags to match | ✅ | ✅1 | ✅1 | ✅ | ❌ | ❌ | ❌ | +| `audio_codec` | Uses the audio codec tags to match | ✅ | ✅1 | ✅1 | ✅ | ❌ | ❌ | ❌ | +| `audio_profile` | Uses the audio profile tags to match | ✅ | ✅1 | ✅1 | ✅ | ❌ | ❌ | ❌ | | `tmdb_genre`2 | Uses the genre from TMDb to match | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | `tmdb_keyword`2 | Uses the keyword from TMDb to match | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | `origin_country`2 | Uses TMDb origin country [ISO 3166-1 alpha-2 codes](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) to match
Example: `origin_country: us` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | @@ -138,6 +142,8 @@ Number filters can **NOT** take multiple values. | Number Modifier | Description | Format | |:----------------|:-------------------------------------------------------------------------------------------|:-------------------------------------------------:| +| No Modifier | Matches every item where the number attribute is equal to the given number | **Format:** number
e.g. `30`, `1995`, or `7.5` | +| `.not` | Matches every item where the number attribute is not equal to the given number | **Format:** number
e.g. `30`, `1995`, or `7.5` | | `.gt` | Matches every item where the number attribute is greater than the given number | **Format:** number
e.g. `30`, `1995`, or `7.5` | | `.gte` | Matches every item where the number attribute is greater than or equal to the given number | **Format:** number
e.g. `30`, `1995`, or `7.5` | | `.lt` | Matches every item where the number attribute is less than the given number | **Format:** number
e.g. `30`, `1995`, or `7.5` | @@ -145,18 +151,24 @@ Number filters can **NOT** take multiple values. ### Attribute -| Number Filters | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track | -|:------------------------------|:---------------------------------------------------------------------|:-------:|:-------:|:--------:|:--------:|:--------:|:--------:|:--------:| -| `year` | Uses the year attribute to match
minimum: `1` | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | -| `tmdb_year`1 | Uses the year on TMDb to match
minimum: `1` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | -| `critic_rating` | Uses the critic rating attribute to match
`0.0` - `10.0` | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | -| `audience_rating` | Uses the audience rating attribute to match
`0.0` - `10.0` | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | -| `user_rating` | Uses the user rating attribute to match
`0.0` - `10.0` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| `tmdb_vote_count`1 | Uses the tmdb vote count to match
minimum: `1` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | -| `plays` | Uses the plays attribute to match
minimum: `1` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | -| `duration` | Uses the duration attribute to match using minutes
minimum: `0.0` | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | +| Number Filters | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track | +|:------------------------------|:---------------------------------------------------------------------|:-------:|:-------------------:|:-------------------:|:--------:|:--------:|:--------:|:--------:| +| `year` | Uses the year attribute to match
minimum: `1` | ✅ | ✅ | ✅ | ✅ | ❌ | ✅ | ✅ | +| `tmdb_year`2 | Uses the year on TMDb to match
minimum: `1` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| `critic_rating` | Uses the critic rating attribute to match
`0.0` - `10.0` | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | +| `audience_rating` | Uses the audience rating attribute to match
`0.0` - `10.0` | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ❌ | +| `user_rating` | Uses the user rating attribute to match
`0.0` - `10.0` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| `tmdb_vote_count`2 | Uses the tmdb vote count to match
minimum: `1` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| `plays` | Uses the plays attribute to match
minimum: `1` | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | +| `duration` | Uses the duration attribute to match using minutes
minimum: `0.0` | ✅ | ✅ | ❌ | ✅ | ❌ | ❌ | ✅ | +| `channels` | Uses the audio channels attribute to match
minimum: `0` | ✅ | ✅1 | ✅1 | ✅ | ❌ | ❌ | ❌ | +| `height` | Uses the height attribute to match
minimum: `0` | ✅ | ✅1 | ✅1 | ✅ | ❌ | ❌ | ❌ | +| `width` | Uses the width attribute to match
minimum: `0` | ✅ | ✅1 | ✅1 | ✅ | ❌ | ❌ | ❌ | +| `aspect` | Uses the aspect attribute to match
minimum: `0.0` | ✅ | ✅1 | ✅1 | ✅ | ❌ | ❌ | ❌ | -1 Also filters out missing movies/shows from being added to Radarr/Sonarr. +1 Filters using the special `episodes` [filter](#special-filters) with the [default percent](details/setting). + +2 Also filters out missing movies/shows from being added to Radarr/Sonarr. ## Special Filters diff --git a/modules/builder.py b/modules/builder.py index 7bea1d7a..6fba176e 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -70,11 +70,15 @@ discover_status = { "Returning Series": "returning", "Planned": "planned", "In Production": "production", "Ended": "ended", "Canceled": "canceled", "Pilot": "pilot" } +sub_filters = [ + "filepath", "audio_track_title", "resolution", "audio_language", "subtitle_language", "has_dolby_vision", + "channels", "height", "width", "aspect", "audio_codec", "audio_profile", "video_codec", "video_profile" +] filters_by_type = { "movie_show_season_episode_artist_album_track": ["title", "summary", "collection", "has_collection", "added", "last_played", "user_rating", "plays", "filepath", "label", "audio_track_title"], "movie_show_season_episode_album_track": ["year"], "movie_show_season_episode_artist_album": ["has_overlay"], - "movie_show_season_episode": ["resolution", "audio_language", "subtitle_language", "has_dolby_vision"], + "movie_show_season_episode": ["resolution", "audio_language", "subtitle_language", "has_dolby_vision", "channels", "height", "width", "aspect", "audio_codec", "audio_profile", "video_codec", "video_profile"], "movie_show_episode_album": ["release", "critic_rating", "history"], "movie_show_episode_track": ["duration"], "movie_show_artist_album": ["genre"], @@ -105,15 +109,18 @@ tmdb_filters = [ string_filters = ["title", "summary", "studio", "record_label", "folder", "filepath", "audio_track_title", "tmdb_title"] string_modifiers = ["", ".not", ".is", ".isnot", ".begins", ".ends", ".regex"] tag_filters = [ - "actor", "collection", "content_rating", "country", "director", "network", "genre", "label", "producer", "year", "origin_country", - "writer", "resolution", "audio_language", "subtitle_language", "tmdb_keyword", "tmdb_genre" + "actor", "collection", "content_rating", "country", "director", "network", "genre", "label", "producer", "year", + "origin_country", "writer", "resolution", "audio_language", "subtitle_language", "tmdb_keyword", "tmdb_genre", + "audio_codec", "audio_profile", "video_codec", "video_profile" ] tag_modifiers = ["", ".not", ".regex", ".count_gt", ".count_gte", ".count_lt", ".count_lte"] boolean_filters = ["has_collection", "has_overlay", "has_dolby_vision"] date_filters = ["release", "added", "last_played", "first_episode_aired", "last_episode_aired"] date_modifiers = ["", ".not", ".before", ".after", ".regex"] -number_filters = ["year", "tmdb_year", "critic_rating", "audience_rating", "user_rating", "tmdb_vote_count", "plays", "duration"] -number_modifiers = [".gt", ".gte", ".lt", ".lte"] +number_filters = [ + "year", "tmdb_year", "critic_rating", "audience_rating", "user_rating", "tmdb_vote_count", "plays", "duration", + "channels", "height", "width", "aspect"] +number_modifiers = ["", ".not", ".gt", ".gte", ".lt", ".lte"] special_filters = [ "history", "episodes", "seasons", "albums", "tracks", "original_language", "original_language.not", "tmdb_status", "tmdb_status.not", "tmdb_type", "tmdb_type.not" @@ -125,7 +132,10 @@ all_filters = boolean_filters + special_filters + \ [f"{f}{m}" for f in number_filters for m in number_modifiers] date_attributes = plex.date_attributes + ["first_episode_aired", "last_episode_aired"] year_attributes = plex.year_attributes + ["tmdb_year"] -number_attributes = plex.number_attributes + ["tmdb_vote_count"] +number_attributes = plex.number_attributes + ["channels", "height", "width"] +tag_attributes = plex.tag_attributes + ["audio_codec", "audio_profile", "video_codec", "video_profile"] +float_attributes = plex.float_attributes + ["aspect"] +boolean_attributes = plex.boolean_attributes + boolean_filters smart_invalid = ["collection_order", "collection_level"] smart_only = ["collection_filtering"] smart_url_invalid = ["filters", "run_again", "sync_mode", "show_filtered", "show_missing", "save_report", "smart_label"] + radarr_details + sonarr_details @@ -1437,7 +1447,7 @@ class CollectionBuilder: final_data = self.validate_attribute(filter_attr, modifier, f"{filter_final} filter", filter_data, validate) if filter_attr in tmdb_filters: self.tmdb_filters.append((filter_final, final_data)) - elif self.collection_level in ["show", "season", "artist", "album"] and filter_attr in ["filepath", "audio_track_title", "resolution", "audio_language", "subtitle_language", "has_dolby_vision"]: + elif self.collection_level in ["show", "season", "artist", "album"] and filter_attr in sub_filters: self.filters.append(("episodes" if self.collection_level in ["show", "season"] else "tracks", {filter_final: final_data, "percentage": self.default_percent})) else: self.filters.append((filter_final, final_data)) @@ -1852,7 +1862,7 @@ class CollectionBuilder: def validate_attribute(self, attribute, modifier, final, data, validate, plex_search=False): def smart_pair(list_to_pair): return [(t, t) for t in list_to_pair] if plex_search else list_to_pair - if attribute in plex.tag_attributes and modifier in [".regex"]: + if attribute in tag_attributes and modifier in [".regex"]: _, names = self.library.get_search_choices(attribute, title=not plex_search, name_pairs=True) valid_list = [] used = [] @@ -1892,7 +1902,7 @@ class CollectionBuilder: return util.parse(self.Type, final, data, datatype="commalist", options=[v for k, v in discover_types.items()]) elif attribute == "tmdb_status": return util.parse(self.Type, final, data, datatype="commalist", options=[v for k, v in discover_status.items()]) - elif attribute in plex.tag_attributes and modifier in ["", ".not"]: + elif attribute in tag_attributes and modifier in ["", ".not"]: if attribute in plex.tmdb_attributes: final_values = [] for value in util.get_list(data): @@ -1948,11 +1958,11 @@ class CollectionBuilder: search_data = util.parse(self.Type, final, data, datatype="int", minimum=0) return f"{search_data}{search_mod}" if plex_search else search_data elif (attribute in number_attributes + year_attributes and modifier in ["", ".not", ".gt", ".gte", ".lt", ".lte"]) \ - or (attribute in plex.tag_attributes and modifier in [".count_gt", ".count_gte", ".count_lt", ".count_lte"]): + or (attribute in tag_attributes and modifier in [".count_gt", ".count_gte", ".count_lt", ".count_lte"]): return util.parse(self.Type, final, data, datatype="int", minimum=0) - elif attribute in plex.float_attributes and modifier in [".gt", ".gte", ".lt", ".lte"]: + elif attribute in float_attributes and modifier in ["", ".not", ".gt", ".gte", ".lt", ".lte"]: return util.parse(self.Type, final, data, datatype="float", minimum=0, maximum=None if attribute == "duration" else 10) - elif attribute in plex.boolean_attributes + boolean_filters: + elif attribute in boolean_attributes: return util.parse(self.Type, attribute, data, datatype="bool") elif attribute in ["seasons", "episodes", "albums", "tracks"]: if isinstance(data, dict) and data: diff --git a/modules/plex.py b/modules/plex.py index 43561d32..b60bb431 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -120,6 +120,13 @@ modifier_translation = { ".before": "%3C%3C", ".after": "%3E%3E", ".begins": "%3C", ".ends": "%3E", ".regex": "" } attribute_translation = { + "aspect": "aspectRatio", + "channels": "audioChannels", + "audio_codec": "audioCodec", + "audio_profile ": "audioProfile", + "video_codec": "videoCodec", + "video_profile": "videoProfile", + "resolution": "videoResolution", "record_label": "studio", "actor": "actors", "audience_rating": "audienceRating", @@ -1257,6 +1264,11 @@ class Plex(Library): for media in item.media: for part in media.parts: values.extend([a.extendedDisplayTitle for a in part.audioStreams() if a.extendedDisplayTitle]) + elif filter_attr in ["audio_codec", "audio_profile", "video_codec", "video_profile"]: + for media in item.media: + attr = getattr(media, filter_actual) + if attr and attr not in values: + values.append(attr) elif filter_attr in ["filepath", "folder"]: values = [loc for loc in item.locations] else: @@ -1322,12 +1334,20 @@ class Plex(Library): failures += 1 if failures > failure_threshold: return False - elif modifier in [".gt", ".gte", ".lt", ".lte", ".count_gt", ".count_gte", ".count_lt", ".count_lte"]: + elif filter_attr in builder.number_filters or modifier in [".gt", ".gte", ".lt", ".lte", ".count_gt", ".count_gte", ".count_lt", ".count_lte"]: divider = 60000 if filter_attr == "duration" else 1 test_number = [] - if filter_attr == "resolution": + if filter_attr in ["resolution", "audio_codec", "audio_profile", "video_codec", "video_profile"]: + for media in item.media: + attr = getattr(media, filter_actual) + if attr and attr not in test_number: + test_number.append(attr) + elif filter_attr in ["channels", "height", "width", "aspect"]: + test_number = 0 for media in item.media: - test_number.append(media.videoResolution) + attr = getattr(media, filter_actual) + if attr and attr > test_number: + test_number = attr elif filter_attr == "audio_language": for media in item.media: for part in media.parts: diff --git a/modules/util.py b/modules/util.py index 4ee29691..363bbeb1 100644 --- a/modules/util.py +++ b/modules/util.py @@ -488,7 +488,9 @@ def is_date_filter(value, modifier, data, final, current_time): return False def is_number_filter(value, modifier, data): - return value is None or (modifier == ".gt" and value <= data) \ + return value is None or (modifier == "" and value == data) \ + or (modifier == ".not" and value != data) \ + or (modifier == ".gt" and value <= data) \ or (modifier == ".gte" and value < data) \ or (modifier == ".lt" and value >= data) \ or (modifier == ".lte" and value > data)