From ad31c59cff64cd34110feb21ac317458ee5edb56 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Fri, 26 Mar 2021 01:43:11 -0400 Subject: [PATCH] plex_search update! --- modules/builder.py | 151 ++++++++++++++++++++++++++++----------------- modules/config.py | 4 +- modules/plex.py | 42 ++++++++++--- modules/util.py | 114 ++++++++++++++++++++-------------- requirements.txt | 2 +- 5 files changed, 199 insertions(+), 114 deletions(-) diff --git a/modules/builder.py b/modules/builder.py index 3362246d..14ca80bc 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -305,23 +305,37 @@ class CollectionBuilder: else: raise Failed(f"Collection Error: {method_name} attribute must be either true or false") elif method_name in util.all_details: self.details[method_name] = method_data + elif method_name in ["title", "title.and", "title.not", "title.begins", "title.ends"]: + self.methods.append(("plex_search", [{method_name: util.get_list(method_data, split=False)}])) + elif method_name in ["decade", "year.greater", "year.less"]: + self.methods.append(("plex_search", [{method_name: util.check_year(method_data, current_year, method_name)}])) + elif method_name in ["added.before", "added.after", "originally_available.before", "originally_available.after"]: + self.methods.append(("plex_search", [{method_name: util.check_date(method_data, method_name, return_string=True, plex_date=True)}])) + elif method_name in ["duration.greater", "duration.less", "rating.greater", "rating.less"]: + self.methods.append(("plex_search", [{method_name: util.check_number(method_data, method_name, minimum=0)}])) elif method_name in ["year", "year.not"]: - self.methods.append(("plex_search", [[(method_name, util.get_year_list(self.data[m], method_name))]])) - elif method_name in ["decade", "decade.not"]: - self.methods.append(("plex_search", [[(method_name, util.get_int_list(self.data[m], util.remove_not(method_name)))]])) + self.methods.append(("plex_search", [{method_name: util.get_year_list(method_data, current_year, method_name)}])) elif method_name in util.tmdb_searches: final_values = [] - for value in util.get_list(self.data[m]): + for value in util.get_list(method_data): if value.lower() == "tmdb" and "tmdb_person" in self.details: for name in self.details["tmdb_person"]: final_values.append(name) else: final_values.append(value) - self.methods.append(("plex_search", [[(method_name, final_values)]])) - elif method_name == "title": - self.methods.append(("plex_search", [[(method_name, util.get_list(self.data[m], split=False))]])) + self.methods.append(("plex_search", [{method_name: self.library.validate_search_list(final_values, os.path.splitext(method_name)[0])}])) elif method_name in util.plex_searches: - self.methods.append(("plex_search", [[(method_name, util.get_list(self.data[m]))]])) + if method_name in util.tmdb_searches: + final_values = [] + for value in util.get_list(method_data): + if value.lower() == "tmdb" and "tmdb_person" in self.details: + for name in self.details["tmdb_person"]: + final_values.append(name) + else: + final_values.append(value) + else: + final_values = method_data + self.methods.append(("plex_search", [{method_name: self.library.validate_search_list(final_values, os.path.splitext(method_name)[0])}])) elif method_name == "plex_all": self.methods.append((method_name, [""])) elif method_name == "plex_collection": @@ -436,31 +450,63 @@ class CollectionBuilder: new_dictionary["exclude"] = exact_list self.methods.append((method_name, [new_dictionary])) elif method_name == "plex_search": - searches = [] - used = [] - for s in self.data[m]: - if s in util.method_alias or (s.endswith(".not") and s[:-4] in util.method_alias): - search = (util.method_alias[s[:-4]] + s[-4:]) if s.endswith(".not") else util.method_alias[s] - logger.warning(f"Collection Warning: {s} plex search attribute will run as {search}") - else: - search = s - if search in util.movie_only_searches and self.library.is_show: - raise Failed(f"Collection Error: {search} plex search attribute only works for movie libraries") - elif util.remove_not(search) in used: - raise Failed(f"Collection Error: Only one instance of {search} can be used try using it as a filter instead") - elif search in ["year", "year.not"]: - years = util.get_year_list(self.data[m][s], search) - if len(years) > 0: - used.append(util.remove_not(search)) - searches.append((search, util.get_int_list(self.data[m][s], util.remove_not(search)))) - elif search == "title": - used.append(util.remove_not(search)) - searches.append((search, util.get_list(self.data[m][s], split=False))) - elif search in util.plex_searches: - used.append(util.remove_not(search)) - searches.append((search, util.get_list(self.data[m][s]))) + searches = {} + for search_name, search_data in method_data: + search, modifier = os.path.splitext(str(search_name).lower()) + if search in util.method_alias: + search = util.method_alias[search] + logger.warning(f"Collection Warning: {str(search_name).lower()} plex search attribute will run as {search}{modifier if modifier else ''}") + search_final = f"{search}{modifier}" + if search_final in util.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_data is None: + raise Failed(f"Collection Error: {search_final} plex search attribute is blank") + elif search == "sort_by": + if str(search_data).lower() in util.plex_sort: + searches[search] = str(search_data).lower() + else: + logger.warning(f"Collection Error: {search_data} is not a valid plex search sort defaulting to title.asc") + elif search == "limit": + if not search_data: + raise Failed(f"Collection Warning: plex search limit attribute is blank") + elif not isinstance(search_data, int) and search_data > 0: + 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 == "title" and modifier in ["", ".and", ".not", ".begins", ".ends"]: + searches[search_final] = util.get_list(search_data, split=False) + elif (search == "studio" and modifier in ["", ".and", ".not", ".begins", ".ends"]) \ + or (search in ["actor", "audio_language", "collection", "content_rating", "country", "director", "genre", "label", "producer", "subtitle_language", "writer"] and modifier in ["", ".and", ".not"]) \ + or (search == "resolution" and modifier in [""]): + if search_final in util.tmdb_searches: + final_values = [] + for value in util.get_list(search_data): + if value.lower() == "tmdb" and "tmdb_person" in self.details: + for name in self.details["tmdb_person"]: + final_values.append(name) + else: + final_values.append(value) + else: + final_values = search_data + searches[search_final] = self.library.validate_search_list(final_values, search) + elif (search == "decade" and modifier in [""]) \ + or (search == "year" and modifier in [".greater", ".less"]): + searches[search_final] = util.check_year(search_data, current_year, search_final) + elif search in ["added", "originally_available"] and modifier in [".before", ".after"]: + searches[search_final] = util.check_date(search_data, search_final, return_string=True, plex_date=True) + elif search in ["duration", "rating"] and modifier in [".greater", ".less"]: + searches[search_final] = util.check_number(search_data, search_final, minimum=0) + elif search == "year" and modifier in ["", ".not"]: + searches[search_final] = util.get_year_list(search_data, current_year, search_final) + elif (search in ["title", "studio"] and modifier not in ["", ".and", ".not", ".begins", ".ends"]) \ + or (search in ["actor", "audio_language", "collection", "content_rating", "country", "director", "genre", "label", "producer", "subtitle_language", "writer"] and modifier not in ["", ".and", ".not"]) \ + or (search in ["resolution", "decade"] and modifier not in [""]) \ + or (search in ["added", "originally_available"] and modifier not in [".before", ".after"]) \ + or (search in ["duration", "rating"] and modifier not in [".greater", ".less"]) \ + or (search in ["year"] and modifier not in ["", ".not", ".greater", ".less"]): + raise Failed(f"Collection Error: modifier: {modifier} not supported with the {search} plex search attribute") else: - logger.error(f"Collection Error: {search} plex search attribute not supported") + raise Failed(f"Collection Error: {search_final} plex search attribute not supported") self.methods.append((method_name, [searches])) elif method_name == "tmdb_discover": new_dictionary = {"limit": 100} @@ -758,38 +804,31 @@ class CollectionBuilder: items_found += len(items) elif method == "plex_search": search_terms = {} - title_searches = None has_processed = False - for search_method, search_list in value: - if search_method == "title": - ors = "" - for o, param in enumerate(search_list): - ors += f"{' OR ' if o > 0 else ''}{param}" - title_searches = search_list - logger.info(f"Processing {pretty}: title({ors})") - has_processed = True - break - for search_method, search_list in value: - if search_method != "title": - final_method = search_method[:-4] + "!" if search_method[-4:] == ".not" else search_method - if self.library.is_show: - final_method = "show." + final_method - search_terms[final_method] = search_list + search_limit = None + search_sort = None + for search_method, search_data in value: + if search_method == "limit": + search_limit = search_data + elif search_method == "sort_by": + search_sort = util.plex_sort[search_data] + else: + search, modifier = os.path.splitext(str(search_method).lower()) + final_search = util.search_alias[search] if search in util.search_alias else search + final_mod = util.plex_modifiers[modifier] if modifier in util.plex_modifiers else "" + final_method = f"{final_search}{final_mod}" + search_terms[final_method] = search_data * 60000 if final_search == "duration" else search_data ors = "" - for o, param in enumerate(search_list): - or_des = " OR " if o > 0 else f"{search_method}(" + conjunction = " AND " if final_mod == "&" else " OR " + for o, param in enumerate(search_data): + or_des = conjunction if o > 0 else f"{search_method}(" ors += f"{or_des}{param}" if has_processed: logger.info(f"\t\t AND {ors})") else: logger.info(f"Processing {pretty}: {ors})") has_processed = True - if title_searches: - items = [] - for title_search in title_searches: - items.extend(self.library.Plex.search(title_search, **search_terms)) - else: - items = self.library.Plex.search(**search_terms) + items = self.library.Plex.search(sort=search_sort, maxresults=search_limit, **search_terms) items_found += len(items) elif method == "plex_collectionless": good_collections = [] diff --git a/modules/config.py b/modules/config.py index 69ea0c2c..6789a253 100644 --- a/modules/config.py +++ b/modules/config.py @@ -106,8 +106,8 @@ class Config: if isinstance(data[attribute], bool): return data[attribute] else: message = f"{text} must be either true or false" elif var_type == "int": - if isinstance(data[attribute], int) and data[attribute] > 0: return data[attribute] - else: message = f"{text} must an integer > 0" + if isinstance(data[attribute], int) and data[attribute] >= 0: return data[attribute] + else: message = f"{text} must an integer >= 0" elif var_type == "path": if os.path.exists(os.path.abspath(data[attribute])): return data[attribute] else: message = f"Path {os.path.abspath(data[attribute])} does not exist" diff --git a/modules/plex.py b/modules/plex.py index 66dc5c55..df2011fd 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -14,25 +14,34 @@ logger = logging.getLogger("Plex Meta Manager") class PlexAPI: def __init__(self, params, TMDb, TVDb): - try: self.PlexServer = PlexServer(params["plex"]["url"], params["plex"]["token"], timeout=params["plex"]["timeout"]) - except Unauthorized: raise Failed("Plex Error: Plex token is invalid") - except ValueError as e: raise Failed(f"Plex Error: {e}") + try: + self.PlexServer = PlexServer(params["plex"]["url"], params["plex"]["token"], timeout=params["plex"]["timeout"]) + except Unauthorized: + raise Failed("Plex Error: Plex token is invalid") + except ValueError as e: + raise Failed(f"Plex Error: {e}") except requests.exceptions.ConnectionError: util.print_stacktrace() raise Failed("Plex Error: Plex url is invalid") self.is_movie = params["library_type"] == "movie" self.is_show = params["library_type"] == "show" self.Plex = next((s for s in self.PlexServer.library.sections() if s.title == params["name"] and ((self.is_movie and isinstance(s, MovieSection)) or (self.is_show and isinstance(s, ShowSection)))), None) - if not self.Plex: raise Failed(f"Plex Error: Plex Library {params['name']} not found") - try: self.data, ind, bsi = yaml.util.load_yaml_guess_indent(open(params["metadata_path"], encoding="utf-8")) - except yaml.scanner.ScannerError as e: raise Failed(f"YAML Error: {util.tab_new_lines(e)}") + if not self.Plex: + raise Failed(f"Plex Error: Plex Library {params['name']} not found") + try: + self.data, ind, bsi = yaml.util.load_yaml_guess_indent(open(params["metadata_path"], encoding="utf-8")) + except yaml.scanner.ScannerError as e: + raise Failed(f"YAML Error: {util.tab_new_lines(e)}") def get_dict(attribute): if attribute in self.data: if self.data[attribute]: - if isinstance(self.data[attribute], dict): return self.data[attribute] - else: logger.warning(f"Config Warning: {attribute} must be a dictionary") - else: logger.warning(f"Config Warning: {attribute} attribute is blank") + if isinstance(self.data[attribute], dict): + return self.data[attribute] + else: + logger.warning(f"Config Warning: {attribute} must be a dictionary") + else: + logger.warning(f"Config Warning: {attribute} attribute is blank") return None self.metadata = get_dict("metadata") @@ -81,6 +90,21 @@ class PlexAPI: def server_search(self, data): return self.PlexServer.search(data) + def get_search_choices(self, search_name, key=False): + if key: return {c.key.lower(): c.key for c in self.Plex.listFilterChoices(search_name)} + else: return {c.title.lower(): c.title for c in self.Plex.listFilterChoices(search_name)} + + def validate_search_list(self, data, search_name): + final_search = util.search_alias[search_name] if search_name in util.search_alias else search_name + search_choices = self.get_search_choices(final_search, key=final_search.endswith("Language")) + valid_list = [] + for value in util.get_list(data): + if str(value).lower in search_choices: + valid_list.append(search_choices[str(value).lower]) + else: + raise Failed(f"Plex Error: No {search_name}: {value} found") + return valid_list + def get_all_collections(self): return self.Plex.search(libtype="collection") diff --git a/modules/util.py b/modules/util.py index db566876..99e45c4c 100644 --- a/modules/util.py +++ b/modules/util.py @@ -29,11 +29,20 @@ method_alias = { "decades": "decade", "directors": "director", "genres": "genre", + "labels": "label", "studios": "studio", "network": "studio", "networks": "studio", "producers": "producer", "writers": "writer", "years": "year" } +search_alias = { + "audio_language": "audioLanguage", + "content_rating": "contentRating", + "subtitle_language": "subtitleLanguage", + "added": "addedAt", + "originally_available": "originallyAvailableAt", + "rating": "userRating" +} filter_alias = { "actor": "actors", "collection": "collections", @@ -330,18 +339,6 @@ dictionary_lists = [ "tautulli_watched", "tmdb_discover" ] -plex_searches = [ - "actor", #"actor.not", # Waiting on PlexAPI to fix issue - "country", #"country.not", - "decade", #"decade.not", - "director", #"director.not", - "genre", #"genre.not", - "producer", #"producer.not", - "studio", #"studio.not", - "title", - "writer", #"writer.not" - "year" #"year.not", -] show_only_lists = [ "tmdb_network", "tmdb_show", @@ -360,20 +357,6 @@ movie_only_lists = [ "tvdb_movie", "tvdb_movie_details" ] -movie_only_searches = [ - "actor", "actor.not", - "country", "country.not", - "decade", "decade.not", - "director", "director.not", - "producer", "producer.not", - "writer", "writer.not" -] -tmdb_searches = [ - "actor", "actor.not", - "director", "director.not", - "producer", "producer.not", - "writer", "writer.not" -] count_lists = [ "anidb_popular", "anilist_popular", @@ -452,6 +435,59 @@ tmdb_type = { "tmdb_writer": "Person", "tmdb_writer_details": "Person" } +plex_searches = [ + "title", "title.and", "title.not", "title.begins", "title.ends", + "studio", "studio.and", "studio.not", "studio.begins", "studio.ends", + "actor", "actor.and", "actor.not", + "audio_language", "audio_language.and", "audio_language.not", + "collection", "collection.and", "collection.not", + "content_rating", "content_rating.and", "content_rating.not", + "country", "country.and", "country.not", + "director", "director.and", "director.not", + "genre", "genre.and", "genre.not", + "label", "label.and", "label.not", + "producer", "producer.and", "producer.not", + "subtitle_language", "subtitle_language.and", "subtitle_language.not", + "writer", "writer.and", "writer.not", + "decade", "resolution", + "added.before", "added.after", + "originally_available.before", "originally_available.after", + "duration.greater", "duration.less", + "rating.greater", "rating.less", + "year", "year.not", "year.greater", "year.less" +] +plex_sort = { + "title.asc": "titleSort:asc", "title.desc": "titleSort:desc", + "originally_available.asc": "originallyAvailableAt:asc", "originally_available.desc": "originallyAvailableAt:desc", + "critic_rating.asc": "rating:asc", "critic_rating.desc": "rating:desc", + "audience_rating.asc": "audienceRating:asc", "audience_rating.desc": "audienceRating:desc", + "duration.asc": "duration:asc", "duration.desc": "duration:desc", + "added.asc": "addedAt:asc", "added.desc": "addedAt:desc" +} +plex_modifiers = { + ".and": "&", + ".not": "!", + ".begins": "<", + ".ends": ">", + ".before": "<<", + ".after": ">>", + ".greater": ">>", + ".less": "<<" +} +movie_only_searches = [ + "audio_language", "audio_language.and", "audio_language.not", + "country", "country.and", "country.not", + "subtitle_language", "subtitle_language.and", "subtitle_language.not", + "decade", "resolution", + "originally_available.before", "originally_available.after", + "duration.greater", "duration.less" +] +tmdb_searches = [ + "actor", "actor.and", "actor.not", + "director", "director.and", "director.not", + "producer", "producer.and", "producer.not", + "writer", "writer.and", "writer.not" +] all_filters = [ "actor", "actor.not", "audio_language", "audio_language.not", @@ -612,25 +648,11 @@ def get_int_list(data, id_type): except Failed as e: logger.error(e) return int_values -def get_year_list(data, method): - values = get_list(data) +def get_year_list(data, current_year, method): final_years = [] - current_year = datetime.now().year + values = get_list(data) for value in values: - try: - if "-" in value: - year_range = re.search("(\\d{4})-(\\d{4}|NOW)", str(value)) - start = check_year(year_range.group(1), current_year, method) - end = current_year if year_range.group(2) == "NOW" else check_year(year_range.group(2), current_year, method) - if int(start) > int(end): - raise Failed(f"Collection Error: {method} starting year: {start} cannot be greater then ending year {end}") - else: - for i in range(int(start), int(end) + 1): - final_years.append(int(i)) - else: - final_years.append(check_year(value, current_year, method)) - except AttributeError: - raise Failed(f"Collection Error: {method} failed to parse year from {value}") + final_years.append(check_year(value, current_year, method)) return final_years def check_year(year, current_year, method): @@ -653,9 +675,9 @@ def check_number(value, method, number_type="int", minimum=None, maximum=None): else: return num_value -def check_date(date_text, method, return_string=False): - try: date_obg = datetime.strptime(str(date_text), "%m/%d/%Y") - except ValueError: raise Failed(f"Collection Error: {method}: {date_text} must match pattern MM/DD/YYYY e.g. 12/25/2020") +def check_date(date_text, method, return_string=False, plex_date=False): + try: date_obg = datetime.strptime(str(date_text), "%Y/%m/%d" if plex_date else "%m/%d/%Y") + except ValueError: raise Failed(f"Collection Error: {method}: {date_text} must match pattern {'YYYY/MM/DD e.g. 2020/12/25' if plex_date else 'MM/DD/YYYY e.g. 12/25/2020'}") return str(date_text) if return_string else date_obg def logger_input(prompt, timeout=60): diff --git a/requirements.txt b/requirements.txt index 56a3237e..4b08a6e0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # Remove # Less common, pinned -PlexAPI==4.5.0 +PlexAPI==4.5.1 tmdbv3api==1.7.5 trakt.py==4.3.0 # More common, flexible