diff --git a/modules/builder.py b/modules/builder.py index aa37018f..c013421a 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -25,11 +25,12 @@ method_alias = { "labels": "label", "rating": "critic_rating", "show_user_rating": "user_rating", + "video_resolution": "resolution", "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", + "released": "release", "show_released": "release", "max_age": "release", "studios": "studio", "networks": "network", "producers": "producer", @@ -49,7 +50,9 @@ filter_translation = { "label": "labels", "producer": "producers", "release": "originallyAvailableAt", - "tmdb_vote_count": "vote_count", + "added": "addedAt", + "last_played": "lastViewedAt", + "plays": "viewCount", "user_rating": "userRating", "writer": "writers" } @@ -155,27 +158,31 @@ boolean_details = [ all_filters = [ "actor", "actor.not", "audio_language", "audio_language.not", - "audio_track_title", "audio_track_title.not", + "audio_track_title", "audio_track_title.not", "audio_track_title.begins", "audio_track_title.ends", "collection", "collection.not", "content_rating", "content_rating.not", "country", "country.not", "director", "director.not", "filepath", "filepath.not", "genre", "genre.not", - "max_age", - "release.gte", "release.lte", - "title", "title.not", - "tmdb_vote_count.gte", "tmdb_vote_count.lte", - "duration.gte", "duration.lte", + "label", "label.not", + "producer", "producer.not", + "release", "release.not", "release.before", "release.after", + "added", "added.not", "added.before", "added.after", + "last_played", "last_played.not", "last_played.before", "last_played.after", + "title", "title.not", "title.begins", "title.ends", + "plays.gt", "plays.gte", "plays.lt", "plays.lte", + "tmdb_vote_count.gt", "tmdb_vote_count.gte", "tmdb_vote_count.lt", "tmdb_vote_count.lte", + "duration.gt", "duration.gte", "duration.lt", "duration.lte", "original_language", "original_language.not", - "user_rating.gte", "user_rating.lte", - "audience_rating.gte", "audience_rating.lte", - "critic_rating.gte", "critic_rating.lte", - "studio", "studio.not", + "user_rating.gt", "user_rating.gte", "user_rating.lt", "user_rating.lte", + "audience_rating.gt", "audience_rating.gte", "audience_rating.lt", "audience_rating.lte", + "critic_rating.gt", "critic_rating.gte", "critic_rating.lt", "critic_rating.lte", + "studio", "studio.not", "studio.begins", "studio.ends", "subtitle_language", "subtitle_language.not", - "video_resolution", "video_resolution.not", + "resolution", "resolution.not", "writer", "writer.not", - "year", "year.gte", "year.lte", "year.not" + "year", "year.gt", "year.gte", "year.lt", "year.lte", "year.not" ] movie_only_filters = [ "audio_language", "audio_language.not", @@ -185,9 +192,10 @@ movie_only_filters = [ "duration.gte", "duration.lte", "original_language", "original_language.not", "subtitle_language", "subtitle_language.not", - "video_resolution", "video_resolution.not", + "resolution", "resolution.not", "writer", "writer.not" ] +show_only_filters = ["network"] class CollectionBuilder: def __init__(self, config, library, metadata, name, data): @@ -588,13 +596,13 @@ class CollectionBuilder: validation = self.validate_attribute(smart, smart_mod, smart_final, smart_data, validate, smart=True) if validation is None: continue - elif smart in plex.date_searches and smart_mod in ["", ".not"]: + elif smart in plex.date_attributes and smart_mod in ["", ".not"]: last_text = "is not in the last" if smart_mod == ".not" else "is in the last" last_mod = "%3E%3E" if smart_mod == "" else "%3C%3C" results, display_add = build_url_arg(f"-{validation}d", mod=last_mod, arg_s=f"{validation} Days", mod_s=last_text) elif smart == "duration" and smart_mod in [".gt", ".gte", ".lt", ".lte"]: results, display_add = build_url_arg(validation * 60000) - elif smart in plex.boolean_searches: + elif smart in plex.boolean_attributes: bool_mod = "" if validation else "!" bool_arg = "true" if validation else "false" results, display_add = build_url_arg(1, mod=bool_mod, arg_s=bool_arg, mod_s="is") @@ -862,40 +870,25 @@ class CollectionBuilder: logger.warning(f"Collection Warning: {parent} {methods_in[int_method]} attribute {data_in[methods_in[int_method]]} invalid must an integer >= {minimum} using {default_in} as default") return default_in if method_name == "filters": - for filter_name, filter_data in method_data.items(): - modifier = filter_name[-4:].lower() - modifier = modifier if modifier in [".not", ".lte", ".gte"] else "" - method = filter_name[:-4].lower() if modifier in [".not", ".lte", ".gte"] else filter_name.lower() - if method in method_alias: - filter_method = f"{method_alias[method]}{modifier}" - logger.warning(f"Collection Warning: {filter_name} filter will run as {filter_method}") - else: - filter_method = f"{method}{modifier}" - if filter_method in movie_only_filters and self.library.is_show: - raise Failed(f"Collection Error: {filter_method} filter only works for movie libraries") - elif filter_data is None: - raise Failed(f"Collection Error: {filter_method} filter is blank") - elif filter_method == "year": - valid_data = util.get_year_list(filter_data, self.current_year, f"{filter_method} filter") - elif filter_method in ["max_age", "duration.gte", "duration.lte", "tmdb_vote_count.gte", "tmdb_vote_count.lte"]: - valid_data = util.check_number(filter_data, f"{filter_method} filter", minimum=1) - elif filter_method in ["year.gte", "year.lte"]: - valid_data = util.check_year(filter_data, self.current_year, f"{filter_method} filter") - elif filter_method in ["user_rating.gte", "user_rating.lte", "audience_rating.gte", "audience_rating.lte", "critic_rating.gte", "critic_rating.lte"]: - valid_data = util.check_number(filter_data, f"{filter_method} filter", number_type="float", minimum=0.1, maximum=10) - elif filter_method in ["release.gte", "release.lte"]: - valid_data = util.check_date(filter_data, f"{filter_method} filter") - elif filter_method in ["original_language", "original_language.not"]: - valid_data = util.get_list(filter_data, lower=True) - elif filter_method in ["collection", "collection.not"]: - valid_data = filter_data if isinstance(filter_data, list) else [filter_data] - elif filter_method in ["title", "title.not"]: - valid_data = util.get_list(filter_data, split=False) - elif filter_method in all_filters: - valid_data = util.get_list(filter_data) + validate = True + if "validate" in method_data: + if method_data["validate"] is None: + raise Failed("Collection Error: validate filter attribute is blank") + if not isinstance(method_data["validate"], bool): + raise Failed("Collection Error: validate filter attribute must be either true or false") + validate = method_data["validate"] + for filter_method, filter_data in method_data.items(): + filter_attr, modifier, filter_final = self._split(filter_method) + if filter_final not in all_filters: + raise Failed(f"Collection Error: {filter_final} is not a valid filter attribute") + elif filter_final in movie_only_filters and self.library.is_show: + raise Failed(f"Collection Error: {filter_final} filter attribute only works for movie libraries") + elif filter_final in show_only_filters and self.library.is_movie: + raise Failed(f"Collection Error: {filter_final} filter attribute only works for show libraries") + elif filter_final is None: + raise Failed(f"Collection Error: {filter_final} filter attribute is blank") else: - raise Failed(f"Collection Error: {filter_method} filter not supported") - self.filters.append((filter_method, valid_data)) + self.filters.append((filter_final, self.validate_attribute(filter_attr, modifier, f"{filter_final} filter", filter_data, validate))) elif method_name == "plex_collectionless": new_dictionary = {} dict_methods = {dm.lower(): dm for dm in method_data} @@ -928,7 +921,9 @@ class CollectionBuilder: validate = method_data["validate"] for search_name, search_data in method_data.items(): search, modifier, search_final = self._split(search_name) - if search_final in plex.movie_only_searches and self.library.is_show: + if search_final not in plex.searches: + raise Failed(f"Collection Error: {search_final} is not a valid plex search attribute") + elif search_final in plex.movie_only_searches and self.library.is_show: raise Failed(f"Collection Error: {search_final} plex search attribute only works for movie libraries") elif search_final in plex.show_only_searches and self.library.is_movie: raise Failed(f"Collection Error: {search_final} plex search attribute only works for show libraries") @@ -946,8 +941,6 @@ class CollectionBuilder: raise Failed(f"Collection Warning: plex search limit attribute: {search_data} must be an integer greater then 0") else: searches[search] = search_data - elif search_final not in plex.searches: - raise Failed(f"Collection Error: {search_final} is not a valid plex search attribute") else: searches[search_final] = self.validate_attribute(search, modifier, search_final, search_data, validate) if len(searches) > 0: @@ -1277,10 +1270,14 @@ class CollectionBuilder: def validate_attribute(self, attribute, modifier, final, data, validate, smart=False): def smart_pair(list_to_pair): return [(t, t) for t in list_to_pair] if smart else list_to_pair - if attribute in ["title", "studio", "episode_title"] and modifier in ["", ".and", ".not", ".begins", ".ends"]: + if attribute in ["title", "studio", "episode_title", "audio_track_title"] and modifier in ["", ".and", ".not", ".begins", ".ends"]: return smart_pair(util.get_list(data, split=False)) - elif attribute in plex.tags and modifier in ["", ".and", ".not", ".begins", ".ends"]: - if final in plex.tmdb_searches: + elif attribute == "original_language": + return util.get_list(data, lower=True) + elif attribute == "filepath": + return util.get_list(data) + elif attribute in plex.tags and modifier in ["", ".and", ".not"]: + if attribute in plex.tmdb_attributes: final_values = [] for value in util.get_list(data): if value.lower() == "tmdb" and "tmdb_person" in self.details: @@ -1299,18 +1296,18 @@ class CollectionBuilder: logger.error(e) elif attribute in ["year", "episode_year"] and modifier in [".gt", ".gte", ".lt", ".lte"]:# return util.check_year(data, self.current_year, final) - elif attribute in ["added", "episode_added", "release", "episode_air_date"] and modifier in [".before", ".after"]:# + elif attribute in plex.date_attributes and modifier in [".before", ".after"]:# return util.check_date(data, final, return_string=True, plex_date=True) - elif attribute in ["plays", "episode_plays", "added", "episode_added", "release", "episode_air_date", "duration"] and modifier in ["", ".not", ".gt", ".gte", ".lt", ".lte"]: + elif attribute in plex.number_attributes and modifier in ["", ".not", ".gt", ".gte", ".lt", ".lte"]: return util.check_number(data, final, minimum=1) - elif attribute in ["user_rating", "episode_user_rating", "critic_rating", "audience_rating"] and modifier in [".gt", ".gte", ".lt", ".lte"]: + elif attribute in plex.float_attributes and modifier in [".gt", ".gte", ".lt", ".lte"]: return util.check_number(data, final, number_type="float", minimum=0, maximum=10) elif attribute in ["decade", "year", "episode_year"] and modifier in ["", ".not"]: return smart_pair(util.get_year_list(data, self.current_year, final)) - elif attribute in plex.boolean_searches: + elif attribute in plex.boolean_attributes: return util.get_bool(attribute, data) else: - raise Failed(f"Collection Error: modifier: {modifier} not supported with the {attribute} attribute") + raise Failed(f"Collection Error: {final} attribute not supported") def _split(self, text): attribute, modifier = os.path.splitext(str(text).lower()) @@ -1363,23 +1360,50 @@ class CollectionBuilder: def check_filters(self, current, display): if self.filters: util.print_return(f"Filtering {display} {current.title}") + current_date = datetime.now() for filter_method, filter_data in self.filters: - modifier = filter_method[-4:] - method = filter_method[:-4] if modifier in [".not", ".lte", ".gte"] else filter_method - method_name = filter_translation[method] if method in filter_translation else method - if method_name == "max_age": - threshold_date = datetime.now() - timedelta(days=filter_data) - if current.originallyAvailableAt is None or current.originallyAvailableAt < threshold_date: + filter_attr, modifier, filter_final = self._split(filter_method) + filter_actual = filter_translation[filter_attr] if filter_attr in filter_translation else filter_attr + if filter_attr in ["release", "added", "last_played"]: + current_data = getattr(current, filter_actual) + if modifier in ["", ".not"]: + threshold_date = current_date - timedelta(days=filter_data) + if (modifier == "" and (current_data is None or current_data < threshold_date)) \ + or (modifier == ".not" and current_data and current_data >= threshold_date): + return False + else: + if (modifier == ".before" and (current_data is None or current_data >= filter_data)) \ + or (modifier == ".after" and (current_data is None or current_data <= filter_data)): + return False + elif filter_attr == "audio_track_title": + jailbreak = False + for media in current.media: + for part in media.parts: + for audio in part.audioStreams(): + for check_title in filter_data: + title = audio.title if audio.title else "" + if (modifier in ["", ".not"] and check_title.lower() in title.lower()) \ + or (modifier == ".begins" and title.lower().startswith(check_title.lower())) \ + or (modifier == ".ends" and title.lower().endswith(check_title.lower())): + jailbreak = True + break + if jailbreak: break + if jailbreak: break + if jailbreak: break + if (jailbreak and modifier == ".not") or (not jailbreak and modifier in ["", ".begins", ".ends"]): return False - elif method_name == "title": + elif filter_attr in ["title", "studio"]: jailbreak = False - for check_title in filter_data: - if check_title.lower() in current.title.lower(): + current_data = getattr(current, filter_actual) + for check_data in filter_data: + if (modifier in ["", ".not"] and check_data.lower() in current_data.lower()) \ + or (modifier == ".begins" and current_data.lower().startswith(check_data.lower())) \ + or (modifier == ".ends" and current_data.lower().endswith(check_data.lower())): jailbreak = True break - if (jailbreak and modifier == ".not") or (not jailbreak and modifier != ".not"): + if (jailbreak and modifier == ".not") or (not jailbreak and modifier in ["", ".begins", ".ends"]): return False - elif method_name == "original_language": + elif filter_attr == "original_language": movie = None for key, value in self.library.movie_map.items(): if current.ratingKey in value: @@ -1391,24 +1415,10 @@ class CollectionBuilder: if movie is None: logger.warning(f"Filter Error: No TMDb ID found for {current.title}") continue - if (modifier == ".not" and movie.original_language in filter_data) or (modifier != ".not" and movie.original_language not in filter_data): - return False - elif method_name == "audio_track_title": - jailbreak = False - for media in current.media: - for part in media.parts: - for audio in part.audioStreams(): - for check_title in filter_data: - title = audio.title if audio.title else "" - if check_title.lower() in title.lower(): - jailbreak = True - break - if jailbreak: break - if jailbreak: break - if jailbreak: break - if (jailbreak and modifier == ".not") or (not jailbreak and modifier != ".not"): + if (modifier == ".not" and movie.original_language in filter_data) \ + or (modifier == "" and movie.original_language not in filter_data): return False - elif method_name == "filepath": + elif filter_attr == "filepath": jailbreak = False for location in current.locations: for check_text in filter_data: @@ -1416,16 +1426,15 @@ class CollectionBuilder: jailbreak = True break if jailbreak: break - if (jailbreak and modifier == ".not") or (not jailbreak and modifier != ".not"): + if (jailbreak and modifier == ".not") or (not jailbreak and modifier == ""): return False - elif modifier in [".gte", ".lte"]: - if method_name == "vote_count": + elif modifier in [".gt", ".gte", ".lt", ".lte"]: + if filter_attr == "tmdb_vote_count": tmdb_item = None for key, value in self.library.movie_map.items(): if current.ratingKey in value: try: - tmdb_item = self.config.TMDb.get_movie( - key) if self.library.is_movie else self.config.TMDb.get_show(key) + tmdb_item = self.config.TMDb.get_movie(key) if self.library.is_movie else self.config.TMDb.get_show(key) break except Failed: pass @@ -1433,29 +1442,35 @@ class CollectionBuilder: logger.warning(f"Filter Error: No TMDb ID found for {current.title}") continue attr = tmdb_item.vote_count + elif filter_attr == "duration": + attr = getattr(current, filter_actual) / 60000 else: - attr = getattr(current, method_name) / 60000 if method_name == "duration" else getattr(current, method_name) - if attr is None or (modifier == ".lte" and attr > filter_data) or (modifier == ".gte" and attr < filter_data): + attr = getattr(current, filter_actual) + if attr is None or (modifier == ".gt" and attr <= filter_data) \ + or (modifier == ".gte" and attr < filter_data) \ + or (modifier == ".lt" and attr >= filter_data) \ + or (modifier == ".lte" and attr > filter_data): return False else: attrs = [] - if method_name in ["video_resolution", "audio_language", "subtitle_language"]: + if filter_attr in ["resolution", "audio_language", "subtitle_language"]: for media in current.media: - if method_name == "video_resolution": + if filter_attr == "resolution": attrs.extend([media.videoResolution]) for part in media.parts: - if method_name == "audio_language": + if filter_attr == "audio_language": attrs.extend([a.language for a in part.audioStreams()]) - if method_name == "subtitle_language": + if filter_attr == "subtitle_language": attrs.extend([s.language for s in part.subtitleStreams()]) - elif method_name in ["contentRating", "studio", "year", "rating", "originallyAvailableAt"]: - attrs = [str(getattr(current, method_name))] - elif method_name in ["actors", "countries", "directors", "genres", "writers", "collections"]: - attrs = [getattr(x, "tag") for x in getattr(current, method_name)] + elif filter_attr in ["content_rating", "year", "rating"]: + attrs = [str(getattr(current, filter_actual))] + elif filter_attr in ["actor", "country", "director", "genre", "label", "producer", "writer", "collection"]: + attrs = [attr.tag for attr in getattr(current, filter_actual)] else: - raise Failed(f"Filter Error: filter: {method_name} not supported") + raise Failed(f"Filter Error: filter: {filter_final} not supported") - if (not list(set(filter_data) & set(attrs)) and modifier != ".not") or (list(set(filter_data) & set(attrs)) and modifier == ".not"): + if (not list(set(filter_data) & set(attrs)) and modifier == "") \ + or (list(set(filter_data) & set(attrs)) and modifier == ".not"): return False util.print_return(f"Filtering {display} {current.title}") return True diff --git a/modules/plex.py b/modules/plex.py index 1edf0152..722416aa 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -129,17 +129,14 @@ show_only_searches = [ "episode_user_rating.gt", "episode_user_rating.gte", "episode_user_rating.lt", "episode_user_rating.lte", "episode_year", "episode_year.not", "episode_year.gt", "episode_year.gte", "episode_year.lt", "episode_year.lte" ] -tmdb_searches = [ - "actor", "actor.and", "actor.not", - "director", "director.and", "director.not", - "producer", "producer.and", "producer.not", - "writer", "writer.and", "writer.not" -] -boolean_searches = [ +number_attributes = ["plays", "episode_plays", "added", "episode_added", "release", "episode_air_date", "duration", "tmdb_vote_count"] +float_attributes = ["user_rating", "episode_user_rating", "critic_rating", "audience_rating"] +boolean_attributes = [ "hdr", "unmatched", "duplicate", "unplayed", "progress", "trash", "unplayed_episodes", "episode_unplayed", "episode_duplicate", "episode_progress", "episode_unmatched", ] -date_searches = ["added", "episode_added", "release", "episode_air_date", "last_played", "episode_last_played"] +tmdb_attributes = ["actor", "director", "producer", "writer"] +date_attributes = ["added", "episode_added", "release", "episode_air_date", "last_played", "episode_last_played"] search_display = { "added": "Date Added", "release": "Release Date", @@ -530,9 +527,9 @@ class PlexAPI: else: search, modifier = os.path.splitext(str(search_method).lower()) final_search = search_translation[search] if search in search_translation else search - if search in date_searches and modifier == "": + if search in date_attributes and modifier == "": final_mod = ">>" - elif search in date_searches and modifier == ".not": + elif search in date_attributes and modifier == ".not": final_mod = "<<" elif search in ["critic_rating", "audience_rating"] and modifier == ".gt": final_mod = "__gt" @@ -544,7 +541,7 @@ class PlexAPI: if search == "duration": search_terms[final_method] = search_data * 60000 - elif search in date_searches and modifier in ["", ".not"]: + elif search in date_attributes and modifier in ["", ".not"]: search_terms[final_method] = f"{search_data}d" else: search_terms[final_method] = search_data