From e2393da47e65073260d5949b002d1599ad1d479e Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Wed, 27 Apr 2022 19:14:52 -0400 Subject: [PATCH] [44] add `episodes`, `seasons`, `albums`, and `tracks` subfilters --- VERSION | 2 +- docs/metadata/filters.md | 16 ++- modules/builder.py | 258 ++++++++------------------------------- modules/plex.py | 247 +++++++++++++++++++++++++++++++++++-- 4 files changed, 301 insertions(+), 222 deletions(-) diff --git a/VERSION b/VERSION index f81d3b9f..c8bd3cf7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.5-develop43 +1.16.5-develop44 diff --git a/docs/metadata/filters.md b/docs/metadata/filters.md index 6b564378..1302a15b 100644 --- a/docs/metadata/filters.md +++ b/docs/metadata/filters.md @@ -157,12 +157,16 @@ Special Filters each have their own set of rules for how they're used. ### Attribute -| Special Filters | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track | -|:--------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|:-------:|:--------:|:--------:|:--------:|:--------:|:--------:| -| `history` | Uses the release date attribute (originally available) to match dates throughout history
`day`: Match the Day and Month to Today's Date
`month`: Match the Month to Today's Date
`1-30`: Match the Day and Month to Today's Date or `1-30` days before Today's Date | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | -| `original_language`/`original_language.not`1 | Uses TMDb original language [ISO 639-1 codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) to match
Example: `original_language: en, ko` | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | -| `tmdb_status`/`tmdb_status.not`1 | Uses TMDb Status to match
**Values:** `returning`, `planned`, `production`, `ended`, `canceled`, `pilot` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | -| `tmdb_type`/`tmdb_type.not`1 | Uses TMDb Type to match
**Values:** `documentary`, `news`, `production`, `miniseries`, `reality`, `scripted`, `talk_show`, `video` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| Special Filters | Description | Movies | Shows | Seasons | Episodes | Artists | Albums | Track | +|:--------------------------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:|:--------:| +| `history` | Uses the release date attribute (originally available) to match dates throughout history
`day`: Match the Day and Month to Today's Date
`month`: Match the Month to Today's Date
`1-30`: Match the Day and Month to Today's Date or `1-30` days before Today's Date | ✅ | ✅ | ❌ | ✅ | ❌ | ✅ | ❌ | +| `episodes` | Uses the item's episodes attributes to match
Use the `percentage` attribute given a number between 0-100 to determine the percentage of an items episodes that must match the sub-filter. | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | +| `seasons` | Uses the item's seasons attributes to match
Use the `percentage` attribute given a number between 0-100 to determine the percentage of an items seasons that must match the sub-filter. | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| `tracks` | Uses the item's tracks attributes to match
Use the `percentage` attribute given a number between 0-100 to determine the percentage of an items tracks that must match the sub-filter. | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ | +| `albums` | Uses the item's albums attributes to match
Use the `percentage` attribute given a number between 0-100 to determine the percentage of an items albums that must match the sub-filter. | ❌ | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | +| `original_language`/`original_language.not`1 | Uses TMDb original language [ISO 639-1 codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) to match
Example: `original_language: en, ko` | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| `tmdb_status`/`tmdb_status.not`1 | Uses TMDb Status to match
**Values:** `returning`, `planned`, `production`, `ended`, `canceled`, `pilot` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| `tmdb_type`/`tmdb_type.not`1 | Uses TMDb Type to match
**Values:** `documentary`, `news`, `production`, `miniseries`, `reality`, `scripted`, `talk_show`, `video` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | 1 Also filters out missing movies/shows from being added to Radarr/Sonarr. diff --git a/modules/builder.py b/modules/builder.py index c6416909..988b8d9e 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -1,5 +1,5 @@ import os, re, time -from datetime import datetime, timedelta +from datetime import datetime from modules import anidb, anilist, flixpatrol, icheckmovies, imdb, letterboxd, mal, plex, radarr, reciperr, sonarr, tautulli, tmdb, trakt, tvdb, mdblist, util from modules.util import Failed, ImageData, NotScheduled, NotScheduledRange from plexapi.audio import Artist, Album, Track @@ -11,65 +11,6 @@ logger = util.logger advance_new_agent = ["item_metadata_language", "item_use_original_title"] advance_show = ["item_episode_sorting", "item_keep_episodes", "item_delete_episodes", "item_season_display", "item_episode_sorting"] -method_alias = { - "actors": "actor", "role": "actor", "roles": "actor", - "show_actor": "actor", "show_actors": "actor", "show_role": "actor", "show_roles": "actor", - "collections": "collection", "plex_collection": "collection", - "show_collections": "collection", "show_collection": "collection", - "content_ratings": "content_rating", "contentRating": "content_rating", "contentRatings": "content_rating", - "countries": "country", - "decades": "decade", - "directors": "director", - "genres": "genre", - "labels": "label", - "collection_minimum": "minimum_items", - "playlist_minimum": "minimum_items", - "rating": "critic_rating", - "show_user_rating": "user_rating", - "video_resolution": "resolution", - "tmdb_trending": "tmdb_trending_daily", - "play": "plays", "show_plays": "plays", "show_play": "plays", "episode_play": "episode_plays", - "originally_available": "release", "episode_originally_available": "episode_air_date", - "episode_release": "episode_air_date", "episode_released": "episode_air_date", - "show_originally_available": "release", "show_release": "release", "show_air_date": "release", - "released": "release", "show_released": "release", "max_age": "release", - "studios": "studio", - "networks": "network", - "producers": "producer", - "writers": "writer", - "years": "year", "show_year": "year", "show_years": "year", - "show_title": "title", "filter": "filters", - "seasonyear": "year", "isadult": "adult", "startdate": "start", "enddate": "end", "averagescore": "score", - "minimum_tag_percentage": "min_tag_percent", "minimumtagrank": "min_tag_percent", "minimum_tag_rank": "min_tag_percent", - "anilist_tag": "anilist_search", "anilist_genre": "anilist_search", "anilist_season": "anilist_search", - "mal_producer": "mal_studio", "mal_licensor": "mal_studio", - "trakt_recommended": "trakt_recommended_weekly", "trakt_watched": "trakt_watched_weekly", "trakt_collected": "trakt_collected_weekly", - "collection_changes_webhooks": "changes_webhooks", - "radarr_add": "radarr_add_missing", "sonarr_add": "sonarr_add_missing", - "trakt_recommended_personal": "trakt_recommendations" -} -filter_translation = { - "record_label": "studio", - "actor": "actors", - "audience_rating": "audienceRating", - "collection": "collections", - "content_rating": "contentRating", - "country": "countries", - "critic_rating": "rating", - "director": "directors", - "genre": "genres", - "label": "labels", - "producer": "producers", - "release": "originallyAvailableAt", - "added": "addedAt", - "last_played": "lastViewedAt", - "plays": "viewCount", - "user_rating": "userRating", - "writer": "writers", - "mood": "moods", - "style": "styles" -} -modifier_alias = {".greater": ".gt", ".less": ".lt"} all_builders = anidb.builders + anilist.builders + flixpatrol.builders + icheckmovies.builders + imdb.builders + \ letterboxd.builders + mal.builders + plex.builders + reciperr.builders + tautulli.builders + \ tmdb.builders + trakt.builders + tvdb.builders + mdblist.builders @@ -137,7 +78,10 @@ filters_by_type = { "movie_show": ["studio", "original_language", "has_overlay", "tmdb_vote_count", "tmdb_year", "tmdb_genre", "tmdb_title", "tmdb_keyword"], "movie_episode": ["director", "producer", "writer", "resolution", "audio_language", "subtitle_language", "has_dolby_vision"], "movie_artist": ["country"], - "show": ["tmdb_status", "tmdb_type", "origin_country", "network", "first_episode_aired", "last_episode_aired"], + "show_season": ["episodes"], + "artist_album": ["tracks"], + "show": ["seasons", "tmdb_status", "tmdb_type", "origin_country", "network", "first_episode_aired", "last_episode_aired"], + "artist": ["albums"], "album": ["record_label"] } filters = { @@ -165,7 +109,10 @@ date_filters = ["release", "added", "last_played", "first_episode_aired", "last_ 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"] -special_filters = ["history", "original_language", "original_language.not", "tmdb_status", "tmdb_status.not", "tmdb_type", "tmdb_type.not"] +special_filters = [ + "history", "episodes", "seasons", "albums", "tracks", "original_language", "original_language.not", + "tmdb_status", "tmdb_status.not", "tmdb_type", "tmdb_type.not" +] all_filters = boolean_filters + special_filters + \ [f"{f}{m}" for f in string_filters for m in string_modifiers] + \ [f"{f}{m}" for f in tag_filters for m in tag_modifiers] + \ @@ -705,7 +652,7 @@ class CollectionBuilder: if method_key.lower() in ignored_details: continue logger.debug("") - method_name, method_mod, method_final = self._split(method_key) + method_name, method_mod, method_final = self.library.split(method_key) if method_name in ignored_details: continue logger.debug(f"Validating Method: {method_key}") @@ -1472,7 +1419,7 @@ class CollectionBuilder: raise Failed(f"{self.Type} Error: validate filter attribute must be either true or false") validate = dict_data.pop(dict_methods["validate"]) for filter_method, filter_data in dict_data.items(): - filter_attr, modifier, filter_final = self._split(filter_method) + filter_attr, modifier, filter_final = self.library.split(filter_method) message = None if filter_final not in all_filters: message = f"{self.Type} Error: {filter_final} is not a valid filter attribute" @@ -1778,7 +1725,7 @@ class CollectionBuilder: indent = f"\n{' ' * level}" conjunction = f"{'and' if is_all else 'or'}=1&" for _key, _data in filter_dict.items(): - attr, modifier, final_attr = self._split(_key) + attr, modifier, final_attr = self.library.split(_key) def build_url_arg(arg, mod=None, arg_s=None, mod_s=None): arg_key = plex.search_translation[attr] if attr in plex.search_translation else attr @@ -1857,7 +1804,7 @@ class CollectionBuilder: base_dict = {} any_dicts = [] for alias_key, alias_value in filter_alias.items(): - _, _, final = self._split(alias_key) + _, _, final = self.library.split(alias_key) if final in plex.and_searches: base_dict[alias_value[:-4]] = plex_filter[alias_value] elif final in plex.or_searches: @@ -1983,27 +1930,43 @@ class CollectionBuilder: 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: return util.parse(self.Type, attribute, data, datatype="bool") + elif attribute in ["seasons", "episodes", "albums", "tracks"]: + if isinstance(data, dict) and data: + percentage = 60 + if "percentage" in data: + if data["percentage"] is None: + logger.warning(f"{self.Type} Warning: percentage filter attribute is blank using 60 as default") + else: + maybe = util.check_num(data["percentage"]) + if maybe < 0 or maybe > 100: + logger.warning(f"{self.Type} Warning: percentage filter attribute must be a number 0-100 using 60 as default") + else: + percentage = maybe + final_filters = {"percentage": percentage} + for filter_method, filter_data in data.items(): + filter_attr, filter_modifier, filter_final = self.library.split(filter_method) + message = None + if filter_final not in all_filters: + message = f"{self.Type} Error: {filter_final} is not a valid filter attribute" + elif filter_attr not in filters[attribute[:-1]] or filter_attr in ["seasons", "episodes", "albums", "tracks"]: + message = f"{self.Type} Error: {filter_final} is not a valid {attribute[:-1]} filter attribute" + elif filter_final is None: + message = f"{self.Type} Error: {filter_final} filter attribute is blank" + elif filter_final != "percentage": + final_filters[filter_final] = self.validate_attribute(filter_attr, filter_modifier, f"{attribute} {filter_final} filter", filter_data, validate) + if message: + if validate: + raise Failed(message) + else: + logger.error(message) + if not final_filters: + raise Failed(f"{self.Type} Error: no filters found under {attribute}") + return final_filters + else: + raise Failed(f"{self.Type} Error: {final} attribute must be a dictionary") else: raise Failed(f"{self.Type} Error: {final} attribute not supported") - def _split(self, text): - attribute, modifier = os.path.splitext(str(text).lower()) - attribute = method_alias[attribute] if attribute in method_alias else attribute - modifier = modifier_alias[modifier] if modifier in modifier_alias else modifier - - if attribute == "add_to_arr": - attribute = "radarr_add_missing" if self.library.is_movie else "sonarr_add_missing" - elif attribute in ["arr_tag", "arr_folder"]: - attribute = f"{'rad' if self.library.is_movie else 'son'}{attribute}" - elif attribute in date_attributes and modifier in [".gt", ".gte"]: - modifier = ".after" - elif attribute in date_attributes and modifier in [".lt", ".lte"]: - modifier = ".before" - final = f"{attribute}{modifier}" - if text != final: - logger.warning(f"Collection Warning: {text} attribute will run as {final}") - return attribute, modifier, final - def fetch_item(self, item): if isinstance(item, (Movie, Show, Season, Episode, Artist, Album, Track)): if item.ratingKey not in self.library.cached_items: @@ -2114,7 +2077,7 @@ class CollectionBuilder: if not date_to_check or date_to_check > self.current_time: return False for filter_method, filter_data in self.tmdb_filters: - filter_attr, modifier, filter_final = self._split(filter_method) + filter_attr, modifier, filter_final = self.library.split(filter_method) if filter_attr in ["tmdb_status", "tmdb_type", "original_language"]: if filter_attr == "tmdb_status": check_value = discover_status[item.status] @@ -2187,129 +2150,8 @@ class CollectionBuilder: return False if not self.check_tmdb_filter(t_id, item.ratingKey in self.library.movie_rating_key_map): return False - for filter_method, filter_data in self.filters: - filter_attr, modifier, filter_final = self._split(filter_method) - filter_actual = filter_translation[filter_attr] if filter_attr in filter_translation else filter_attr - item_type = self.collection_level - if self.collection_level == "item": - if isinstance(item, Movie): - item_type = "movie" - elif isinstance(item, Show): - item_type = "show" - elif isinstance(item, Season): - item_type = "season" - elif isinstance(item, Episode): - item_type = "episode" - elif isinstance(item, Artist): - item_type = "artist" - elif isinstance(item, Album): - item_type = "album" - elif isinstance(item, Track): - item_type = "track" - else: - continue - if filter_attr not in filters[item_type]: - continue - elif filter_attr in date_filters: - if util.is_date_filter(getattr(item, filter_actual), modifier, filter_data, filter_final, self.current_time): - return False - elif filter_attr in string_filters: - values = [] - if filter_attr == "audio_track_title": - 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 == "filepath": - values = [loc for loc in item.locations] - else: - values = [getattr(item, filter_actual)] - if util.is_string_filter(values, modifier, filter_data): - return False - elif filter_attr in boolean_filters: - filter_check = False - if filter_attr == "has_collection": - filter_check = len(item.collections) > 0 - elif filter_attr == "has_overlay": - for label in item.labels: - if label.tag.lower().endswith(" overlay") or label.tag.lower() == "overlay": - filter_check = True - break - elif filter_attr == "has_dolby_vision": - for media in item.media: - for part in media.parts: - for stream in part.videoStreams(): - if stream.DOVIPresent: - filter_check = True - break - if util.is_boolean_filter(filter_data, filter_check): - return False - elif filter_attr == "history": - item_date = item.originallyAvailableAt - if item_date is None: - return False - elif filter_data == "day": - if item_date.month != self.current_time.month or item_date.day != self.current_time.day: - return False - elif filter_data == "month": - if item_date.month != self.current_time.month: - return False - else: - date_match = False - for i in range(filter_data): - check_date = self.current_time - timedelta(days=i) - if item_date.month == check_date.month and item_date.day == check_date.day: - date_match = True - if date_match is False: - return False - elif 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": - for media in item.media: - test_number.append(media.videoResolution) - elif filter_attr == "audio_language": - for media in item.media: - for part in media.parts: - test_number.extend([a.language for a in part.audioStreams()]) - elif filter_attr == "subtitle_language": - for media in item.media: - for part in media.parts: - test_number.extend([s.language for s in part.subtitleStreams()]) - else: - test_number = getattr(item, filter_actual) - if modifier in [".count_gt", ".count_gte", ".count_lt", ".count_lte"]: - test_number = len(test_number) if test_number else 0 - modifier = f".{modifier[7:]}" - if test_number is None or util.is_number_filter(test_number / divider, modifier, filter_data): - return False - else: - attrs = [] - if filter_attr in ["resolution", "audio_language", "subtitle_language"]: - for media in item.media: - if filter_attr == "resolution": - attrs.append(media.videoResolution) - for part in media.parts: - if filter_attr == "audio_language": - attrs.extend([a.language for a in part.audioStreams()]) - if filter_attr == "subtitle_language": - attrs.extend([s.language for s in part.subtitleStreams()]) - elif filter_attr in ["content_rating", "year", "rating"]: - attrs = [getattr(item, filter_actual)] - elif filter_attr in ["actor", "country", "director", "genre", "label", "producer", "writer", "collection", "network"]: - attrs = [attr.tag for attr in getattr(item, filter_actual)] - else: - raise Failed(f"Filter Error: filter: {filter_final} not supported") - if modifier == ".regex": - has_match = False - for reg in filter_data: - for name in attrs: - if re.compile(reg).search(name): - has_match = True - if has_match is False: - return False - elif (not list(set(filter_data) & set(attrs)) and modifier == "") \ - or (list(set(filter_data) & set(attrs)) and modifier == ".not"): - return False + if self.library.check_filters(item, self.filters, self.current_time) is False: + return False return True def run_missing(self): diff --git a/modules/plex.py b/modules/plex.py index 76ac7709..0087eef2 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -1,8 +1,5 @@ -import os, plexapi, requests -from datetime import datetime - -from plexapi.base import PlexObject - +import os, plexapi, re, requests +from datetime import datetime, timedelta from modules import builder, util from modules.library import Library from modules.util import Failed, ImageData @@ -119,6 +116,65 @@ modifier_translation = { "": "", ".not": "!", ".is": "%3D", ".isnot": "!%3D", ".gt": "%3E%3E", ".gte": "%3E", ".lt": "%3C%3C", ".lte": "%3C", ".before": "%3C%3C", ".after": "%3E%3E", ".begins": "%3C", ".ends": "%3E", ".regex": "" } +attribute_translation = { + "record_label": "studio", + "actor": "actors", + "audience_rating": "audienceRating", + "collection": "collections", + "content_rating": "contentRating", + "country": "countries", + "critic_rating": "rating", + "director": "directors", + "genre": "genres", + "label": "labels", + "producer": "producers", + "release": "originallyAvailableAt", + "added": "addedAt", + "last_played": "lastViewedAt", + "plays": "viewCount", + "user_rating": "userRating", + "writer": "writers", + "mood": "moods", + "style": "styles" +} +method_alias = { + "actors": "actor", "role": "actor", "roles": "actor", + "show_actor": "actor", "show_actors": "actor", "show_role": "actor", "show_roles": "actor", + "collections": "collection", "plex_collection": "collection", + "show_collections": "collection", "show_collection": "collection", + "content_ratings": "content_rating", "contentRating": "content_rating", "contentRatings": "content_rating", + "countries": "country", + "decades": "decade", + "directors": "director", + "genres": "genre", + "labels": "label", + "collection_minimum": "minimum_items", + "playlist_minimum": "minimum_items", + "rating": "critic_rating", + "show_user_rating": "user_rating", + "video_resolution": "resolution", + "tmdb_trending": "tmdb_trending_daily", + "play": "plays", "show_plays": "plays", "show_play": "plays", "episode_play": "episode_plays", + "originally_available": "release", "episode_originally_available": "episode_air_date", + "episode_release": "episode_air_date", "episode_released": "episode_air_date", + "show_originally_available": "release", "show_release": "release", "show_air_date": "release", + "released": "release", "show_released": "release", "max_age": "release", + "studios": "studio", + "networks": "network", + "producers": "producer", + "writers": "writer", + "years": "year", "show_year": "year", "show_years": "year", + "show_title": "title", "filter": "filters", + "seasonyear": "year", "isadult": "adult", "startdate": "start", "enddate": "end", "averagescore": "score", + "minimum_tag_percentage": "min_tag_percent", "minimumtagrank": "min_tag_percent", "minimum_tag_rank": "min_tag_percent", + "anilist_tag": "anilist_search", "anilist_genre": "anilist_search", "anilist_season": "anilist_search", + "mal_producer": "mal_studio", "mal_licensor": "mal_studio", + "trakt_recommended": "trakt_recommended_weekly", "trakt_watched": "trakt_watched_weekly", "trakt_collected": "trakt_collected_weekly", + "collection_changes_webhooks": "changes_webhooks", + "radarr_add": "radarr_add_missing", "sonarr_add": "sonarr_add_missing", + "trakt_recommended_personal": "trakt_recommendations" +} +modifier_alias = {".greater": ".gt", ".less": ".lt"} album_sorting_options = {"default": -1, "newest": 0, "oldest": 1, "name": 2} episode_sorting_options = {"default": -1, "oldest": 0, "newest": 1} keep_episodes_options = {"all": 0, "5_latest": 5, "3_latest": 3, "latest": 1, "past_3": -3, "past_7": -7, "past_30": -30} @@ -560,6 +616,11 @@ class Plex(Library): self._users = users return self._users + def manage_recommendations(self): + return [(r.title, r._data.attrib.get('identifier'), r._data.attrib.get('promotedToRecommended'), + r._data.attrib.get('promotedToOwnHome'), r._data.attrib.get('promotedToSharedHome')) + for r in self.Plex.fetchItems(f"/hubs/sections/{self.Plex.key}/manage")] + def alter_collection(self, item, collection, smart_label_collection=False, add=True): if smart_label_collection: self.query_data(item.addLabel if add else item.removeLabel, collection) @@ -787,7 +848,7 @@ class Plex(Library): def edit_tags(self, attr, obj, add_tags=None, remove_tags=None, sync_tags=None, do_print=True): display = "" final = "" - key = builder.filter_translation[attr] if attr in builder.filter_translation else attr + key = attribute_translation[attr] if attr in attribute_translation else attr attr_display = attr.replace("_", " ").title() attr_call = attr_display.replace(" ", "") if add_tags or remove_tags or sync_tags is not None: @@ -873,7 +934,7 @@ class Plex(Library): item_asset_directory = os.path.join(asset_directory[0], folder_name) os.makedirs(item_asset_directory, exist_ok=True) extra = f"\nAsset Directory Created: {item_asset_directory}" - raise Failed(f"Asset Warning: Unable to find asset {'folder' if self.asset_folders else 'file'}: {folder_name if self.asset_folders else file_name}{extra}") + raise Failed(f"Asset Warning: Unable to find asset {'folder' if self.asset_folders else 'file'}: '{folder_name if self.asset_folders else file_name}{extra}'") poster_filter = os.path.join(item_asset_directory, f"{file_name}.*") background_filter = os.path.join(item_asset_directory, "background.*" if file_name == "poster" else f"{file_name}_background.*") @@ -1013,3 +1074,175 @@ class Plex(Library): _recur("tracks") return map_key, attrs + + def split(self, text): + attribute, modifier = os.path.splitext(str(text).lower()) + attribute = method_alias[attribute] if attribute in method_alias else attribute + modifier = modifier_alias[modifier] if modifier in modifier_alias else modifier + + if attribute == "add_to_arr": + attribute = "radarr_add_missing" if self.is_movie else "sonarr_add_missing" + elif attribute in ["arr_tag", "arr_folder"]: + attribute = f"{'rad' if self.is_movie else 'son'}{attribute}" + elif attribute in builder.date_attributes and modifier in [".gt", ".gte"]: + modifier = ".after" + elif attribute in builder.date_attributes and modifier in [".lt", ".lte"]: + modifier = ".before" + final = f"{attribute}{modifier}" + if text != final: + logger.warning(f"Collection Warning: {text} attribute will run as {final}") + return attribute, modifier, final + + def check_filters(self, item, filters_in, current_time): + for filter_method, filter_data in filters_in: + filter_attr, modifier, filter_final = self.split(filter_method) + if self.check_filter(item, filter_attr, modifier, filter_final, filter_data, current_time) is False: + return False + return True + + def check_filter(self, item, filter_attr, modifier, filter_final, filter_data, current_time): + filter_actual = attribute_translation[filter_attr] if filter_attr in attribute_translation else filter_attr + if isinstance(item, Movie): + item_type = "movie" + elif isinstance(item, Show): + item_type = "show" + elif isinstance(item, Season): + item_type = "season" + elif isinstance(item, Episode): + item_type = "episode" + elif isinstance(item, Artist): + item_type = "artist" + elif isinstance(item, Album): + item_type = "album" + elif isinstance(item, Track): + item_type = "track" + else: + return True + if filter_attr not in builder.filters[item_type]: + return True + elif filter_attr in builder.date_filters: + if util.is_date_filter(getattr(item, filter_actual), modifier, filter_data, filter_final, current_time): + return False + elif filter_attr in builder.string_filters: + values = [] + if filter_attr == "audio_track_title": + 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 == "filepath": + values = [loc for loc in item.locations] + else: + values = [getattr(item, filter_actual)] + if util.is_string_filter(values, modifier, filter_data): + return False + elif filter_attr in builder.boolean_filters: + filter_check = False + if filter_attr == "has_collection": + filter_check = len(item.collections) > 0 + elif filter_attr == "has_overlay": + for label in item.labels: + if label.tag.lower().endswith(" overlay") or label.tag.lower() == "overlay": + filter_check = True + break + elif filter_attr == "has_dolby_vision": + for media in item.media: + for part in media.parts: + for stream in part.videoStreams(): + if stream.DOVIPresent: + filter_check = True + break + if util.is_boolean_filter(filter_data, filter_check): + return False + elif filter_attr == "history": + item_date = item.originallyAvailableAt + if item_date is None: + return False + elif filter_data == "day": + if item_date.month != current_time.month or item_date.day != current_time.day: + return False + elif filter_data == "month": + if item_date.month != current_time.month: + return False + else: + date_match = False + for i in range(filter_data): + check_date = current_time - timedelta(days=i) + if item_date.month == check_date.month and item_date.day == check_date.day: + date_match = True + if date_match is False: + return False + elif filter_attr in ["seasons", "episodes", "albums", "tracks"]: + if filter_attr == "seasons": + sub_items = item.seasons() + elif filter_attr == "albums": + sub_items = item.albums() + elif filter_attr == "tracks": + sub_items = item.tracks() + else: + sub_items = item.episodes() + filters_in = [] + percentage = 60 + for sub_atr, sub_data in filter_data.items(): + if sub_atr == "percentage": + percentage = sub_data + else: + filters_in.append((sub_atr, sub_data)) + failure_threshold = len(sub_items) * ((100 - percentage) / 100) + failures = 0 + for sub_item in sub_items: + if self.check_filters(sub_item, filters_in, current_time) is False: + failures += 1 + if failures > failure_threshold: + return False + elif 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": + for media in item.media: + test_number.append(media.videoResolution) + elif filter_attr == "audio_language": + for media in item.media: + for part in media.parts: + test_number.extend([a.language for a in part.audioStreams()]) + elif filter_attr == "subtitle_language": + for media in item.media: + for part in media.parts: + test_number.extend([s.language for s in part.subtitleStreams()]) + else: + test_number = getattr(item, filter_actual) + if modifier in [".count_gt", ".count_gte", ".count_lt", ".count_lte"]: + test_number = len(test_number) if test_number else 0 + modifier = f".{modifier[7:]}" + if test_number is None or util.is_number_filter(test_number / divider, modifier, filter_data): + return False + else: + attrs = [] + if filter_attr in ["resolution", "audio_language", "subtitle_language"]: + for media in item.media: + if filter_attr == "resolution": + attrs.append(media.videoResolution) + for part in media.parts: + if filter_attr == "audio_language": + attrs.extend([a.language for a in part.audioStreams()]) + if filter_attr == "subtitle_language": + attrs.extend([s.language for s in part.subtitleStreams()]) + elif filter_attr in ["content_rating", "year", "rating"]: + attrs = [getattr(item, filter_actual)] + elif filter_attr in ["actor", "country", "director", "genre", "label", "producer", "writer", + "collection", "network"]: + attrs = [attr.tag for attr in getattr(item, filter_actual)] + else: + raise Failed(f"Filter Error: filter: {filter_final} not supported") + if modifier == ".regex": + has_match = False + for reg in filter_data: + for name in attrs: + if re.compile(reg).search(name): + has_match = True + if has_match is False: + return False + elif (not list(set(filter_data) & set(attrs)) and modifier == "") \ + or (list(set(filter_data) & set(attrs)) and modifier == ".not"): + return False + return True \ No newline at end of file