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)