From be872dac65d008aaa0ffec0e36e3a2cf37005b80 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Sat, 20 Feb 2021 00:41:45 -0500 Subject: [PATCH] added templates #19 --- modules/builder.py | 742 +++++++++++++++++++++++++++++++++++++++++++++ modules/config.py | 667 ++-------------------------------------- modules/plex.py | 39 +-- modules/trakt.py | 2 +- modules/util.py | 1 + 5 files changed, 780 insertions(+), 671 deletions(-) create mode 100644 modules/builder.py diff --git a/modules/builder.py b/modules/builder.py new file mode 100644 index 00000000..e22c59e1 --- /dev/null +++ b/modules/builder.py @@ -0,0 +1,742 @@ +import glob, logging, os, re +from datetime import datetime, timedelta +from modules import util +from modules.util import Failed + +logger = logging.getLogger("Plex Meta Manager") + +class CollectionBuilder: + def __init__(self, config, library, name, data): + self.config = config + self.library = library + self.name = name + self.data = data + self.details = {"arr_tag": None} + self.methods = [] + self.filters = [] + self.posters = [] + self.backgrounds = [] + self.schedule = None + + if "template" in data: + if not self.library.templates: + raise Failed("Collection Error: No templates found") + elif not data["template"]: + raise Failed("Collection Error: template attribute is blank") + else: + template_list = data["template"] if isinstance(data["template"], list) else [data["template"]] + for data_template in template_list: + if not isinstance(data_template, dict): + raise Failed("Collection Error: template attribute is not a dictionary") + elif "name" not in data_template: + raise Failed("Collection Error: template sub-attribute name is required") + elif not data_template["name"]: + raise Failed("Collection Error: template sub-attribute name is blank") + elif data_template["name"] not in self.library.templates: + raise Failed("Collection Error: template {} not found".format(data_template["name"])) + elif not isinstance(self.library.templates[data_template["name"]], dict): + raise Failed("Collection Error: template {} is not a dictionary".format(data_template["name"])) + else: + for tm in data_template: + if not data_template[tm]: + raise Failed("Collection Error: template sub-attribute {} is blank".format(data_template[tm])) + + template_name = data_template["name"] + template = self.library.templates[template_name] + default = {} + if "default" in template: + if template["default"]: + if isinstance(template["default"], dict): + for dv in template["default"]: + if template["default"][dv]: + default[dv] = template["default"][dv] + else: + raise Failed("Collection Error: template default sub-attribute {} is blank".format(dv)) + else: + raise Failed("Collection Error: template sub-attribute default is not a dictionary") + else: + raise Failed("Collection Error: template sub-attribute default is blank") + + + + for m in template: + if m not in self.data and m != "default": + if template[m]: + attr = None + def replace_txt(txt): + txt = str(txt) + for tm in data_template: + if tm != "name" and "<<{}>>".format(tm) in txt: + txt = txt.replace("<<{}>>".format(tm), str(data_template[tm])) + if "<>" in txt: + txt = txt.replace("<>", str(self.name)) + for dm in default: + if "<<{}>>".format(dm) in txt: + txt = txt.replace("<<{}>>".format(dm), str(default[dm])) + if txt in ["true", "True"]: return True + elif txt in ["false", "False"]: return False + else: + try: return int(txt) + except ValueError: return txt + if isinstance(template[m], dict): + attr = {} + for sm in template[m]: + if isinstance(template[m][sm], list): + temp_list = [] + for li in template[m][sm]: + temp_list.append(replace_txt(li)) + attr[sm] = temp_list + else: + attr[sm] = replace_txt(template[m][sm]) + elif isinstance(template[m], list): + attr = [] + for li in template[m]: + if isinstance(li, dict): + temp_dict = {} + for sm in li: + temp_dict[sm] = replace_txt(li[sm]) + attr.append(temp_dict) + else: + attr.append(replace_txt(li)) + else: + attr = replace_txt(template[m]) + self.data[m] = attr + else: + raise Failed("Collection Error: template attribute {} is blank".format(m)) + + skip_collection = True + if "schedule" not in data: + skip_collection = False + elif not data["schedule"]: + logger.error("Collection Error: schedule attribute is blank. Running daily") + skip_collection = False + else: + schedule_list = util.get_list(data["schedule"]) + current_time = datetime.now() + next_month = current_time.replace(day=28) + timedelta(days=4) + last_day = next_month - timedelta(days=next_month.day) + for schedule in schedule_list: + run_time = str(schedule).lower() + if run_time.startswith("day") or run_time.startswith("daily"): + skip_collection = False + if run_time.startswith("week") or run_time.startswith("month") or run_time.startswith("year"): + match = re.search("\\(([^)]+)\\)", run_time) + if match: + param = match.group(1) + if run_time.startswith("week"): + if param.lower() in util.days_alias: + weekday = util.days_alias[param.lower()] + self.schedule += "\nScheduled weekly on {}".format(util.pretty_days[weekday]) + if weekday == current_time.weekday(): + skip_collection = False + else: + logger.error("Collection Error: weekly schedule attribute {} invalid must be a day of the weeek i.e. weekly(Monday)".format(schedule)) + elif run_time.startswith("month"): + try: + if 1 <= int(param) <= 31: + self.schedule += "\nScheduled monthly on the {}".format(util.make_ordinal(param)) + if current_time.day == int(param) or (current_time.day == last_day.day and int(param) > last_day.day): + skip_collection = False + else: + logger.error("Collection Error: monthly schedule attribute {} invalid must be between 1 and 31".format(schedule)) + except ValueError: + logger.error("Collection Error: monthly schedule attribute {} invalid must be an integer".format(schedule)) + elif run_time.startswith("year"): + match = re.match("^(1[0-2]|0?[1-9])/(3[01]|[12][0-9]|0?[1-9])$", param) + if match: + month = int(match.group(1)) + day = int(match.group(2)) + self.schedule += "\nScheduled yearly on {} {}".format(util.pretty_months[month], util.make_ordinal(day)) + if current_time.month == month and (current_time.day == day or (current_time.day == last_day.day and day > last_day.day)): + skip_collection = False + else: + logger.error("Collection Error: yearly schedule attribute {} invalid must be in the MM/DD format i.e. yearly(11/22)".format(schedule)) + else: + logger.error("Collection Error: failed to parse schedule: {}".format(schedule)) + else: + logger.error("Collection Error: schedule attribute {} invalid".format(schedule)) + if self.schedule is None: + skip_collection = False + if skip_collection: + raise Failed("Skipping Collection {}".format(c)) + + logger.info("Scanning {} Collection".format(self.name)) + + self.collectionless = "plex_collectionless" in data + + self.sync = self.library.sync_mode == "sync" + if "sync_mode" in data: + if not data["sync_mode"]: logger.warning("Collection Warning: sync_mode attribute is blank using general: {}".format(self.library.sync_mode)) + elif data["sync_mode"] not in ["append", "sync"]: logger.warning("Collection Warning: {} sync_mode invalid using general: {}".format(self.library.sync_mode, data["sync_mode"])) + else: self.sync = data["sync_mode"] == "sync" + + if "tmdb_person" in data: + if data["tmdb_person"]: + valid_names = [] + for tmdb_id in util.get_int_list(data["tmdb_person"], "TMDb Person ID"): + person = config.TMDb.get_person(tmdb_id) + valid_names.append(person.name) + if "summary" not in self.details and hasattr(person, "biography") and person.biography: + self.details["summary"] = person.biography + if "poster" not in self.details and hasattr(person, "profile_path") and person.profile_path: + self.details["poster"] = ("url", "{}{}".format(config.TMDb.image_url, person.profile_path), "tmdb_person") + if len(valid_names) > 0: self.details["tmdb_person"] = valid_names + else: raise Failed("Collection Error: No valid TMDb Person IDs in {}".format(data["tmdb_person"])) + else: + raise Failed("Collection Error: tmdb_person attribute is blank") + + for m in data: + if "tmdb" in m and not config.TMDb: raise Failed("Collection Error: {} requires TMDb to be configured".format(m)) + elif "trakt" in m and not config.Trakt: raise Failed("Collection Error: {} requires Trakt todo be configured".format(m)) + elif "imdb" in m and not config.IMDb: raise Failed("Collection Error: {} requires TMDb or Trakt to be configured".format(m)) + elif "tautulli" in m and not self.library.Tautulli: raise Failed("Collection Error: {} requires Tautulli to be configured".format(m)) + elif "mal" in m and not config.MyAnimeList: raise Failed("Collection Error: {} requires MyAnimeList to be configured".format(m)) + elif data[m] is not None: + logger.debug("") + logger.debug("Method: {}".format(m)) + logger.debug("Value: {}".format(data[m])) + if m in util.method_alias: + method_name = util.method_alias[m] + logger.warning("Collection Warning: {} attribute will run as {}".format(m, method_name)) + else: + method_name = m + if method_name in util.show_only_lists and self.library.is_movie: + raise Failed("Collection Error: {} attribute only works for show libraries".format(method_name)) + elif method_name in util.movie_only_lists and self.library.is_show: + raise Failed("Collection Error: {} attribute only works for movie libraries".format(method_name)) + elif method_name in util.movie_only_searches and self.library.is_show: + raise Failed("Collection Error: {} plex search only works for movie libraries".format(method_name)) + elif method_name not in util.collectionless_lists and self.collectionless: + raise Failed("Collection Error: {} attribute does not work for Collectionless collection".format(method_name)) + elif method_name == "tmdb_summary": + self.details["summary"] = config.TMDb.get_movie_show_or_collection(util.regex_first_int(data[m], "TMDb ID"), self.library.is_movie).overview + elif method_name == "tmdb_description": + self.details["summary"] = config.TMDb.get_list(util.regex_first_int(data[m], "TMDb List ID")).description + elif method_name == "tmdb_biography": + self.details["summary"] = config.TMDb.get_person(util.regex_first_int(data[m], "TMDb Person ID")).biography + elif method_name == "collection_mode": + if data[m] in ["default", "hide", "hide_items", "show_items", "hideItems", "showItems"]: + if data[m] == "hide_items": self.details[method_name] = "hideItems" + elif data[m] == "show_items": self.details[method_name] = "showItems" + else: self.details[method_name] = data[m] + else: + raise Failed("Collection Error: {} collection_mode Invalid\n| \tdefault (Library default)\n| \thide (Hide Collection)\n| \thide_items (Hide Items in this Collection)\n| \tshow_items (Show this Collection and its Items)".format(data[m])) + elif method_name == "collection_order": + if data[m] in ["release", "alpha"]: + self.details[method_name] = data[m] + else: + raise Failed("Collection Error: {} collection_order Invalid\n| \trelease (Order Collection by release dates)\n| \talpha (Order Collection Alphabetically)".format(data[m])) + elif method_name == "url_poster": + self.posters.append(("url", data[m], method_name)) + elif method_name == "tmdb_poster": + self.posters.append(("url", "{}{}".format(config.TMDb.image_url, config.TMDb.get_movie_show_or_collection(util.regex_first_int(data[m], "TMDb ID"), self.library.is_movie).poster_path), method_name)) + elif method_name == "tmdb_profile": + self.posters.append(("url", "{}{}".format(config.TMDb.image_url, config.TMDb.get_person(util.regex_first_int(data[m], "TMDb Person ID")).profile_path), method_name)) + elif method_name == "file_poster": + if os.path.exists(data[m]): self.posters.append(("file", os.path.abspath(data[m]), method_name)) + else: raise Failed("Collection Error: Poster Path Does Not Exist: {}".format(os.path.abspath(data[m]))) + elif method_name == "url_background": + self.backgrounds.append(("url", data[m], method_name)) + elif method_name == "tmdb_background": + self.backgrounds.append(("url", "{}{}".format(config.TMDb.image_url, config.TMDb.get_movie_show_or_collection(util.regex_first_int(data[m], "TMDb ID"), self.library.is_movie).poster_path), method_name)) + elif method_name == "file_background": + if os.path.exists(data[m]): self.backgrounds.append(("file", os.path.abspath(data[m]), method_name)) + else: raise Failed("Collection Error: Background Path Does Not Exist: {}".format(os.path.abspath(data[m]))) + elif method_name in ["add_to_arr", "show_filtered"]: + if isinstance(data[m], bool): self.details[method_name] = data[m] + else: raise Failed("Collection Error: {} must be either true or false".format(method_name)) + elif method_name == "arr_tag": + self.details[method_name] = util.get_list(data[m]) + elif method_name in util.all_details: + self.details[method_name] = data[m] + elif method_name in ["year", "year.not"]: + self.methods.append(("plex_search", [[(method_name, util.get_year_list(data[m], method_name))]])) + elif method_name in ["decade", "decade.not"]: + self.methods.append(("plex_search", [[(method_name, util.get_int_list(data[m], util.remove_not(method_name)))]])) + elif method_name in util.tmdb_searches: + final_values = [] + for value in util.get_list(data[m]): + 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 in util.plex_searches: + self.methods.append(("plex_search", [[(method_name, util.get_list(data[m]))]])) + elif method_name == "plex_all": + self.methods.append((method_name, [""])) + elif method_name == "plex_collection": + self.methods.append((method_name, self.library.validate_collections(data[m] if isinstance(data[m], list) else [data[m]]))) + elif method_name == "anidb_popular": + list_count = util.regex_first_int(data[m], "List Size", default=40) + if 1 <= list_count <= 30: + self.methods.append((method_name, [list_count])) + else: + logger.warning("Collection Error: anidb_popular must be an integer between 1 and 30 defaulting to 30") + self.methods.append((method_name, [30])) + elif method_name == "mal_id": + self.methods.append((method_name, util.get_int_list(data[m], "MyAnimeList ID"))) + elif method_name in ["anidb_id", "anidb_relation"]: + self.methods.append((method_name, config.AniDB.validate_anidb_list(util.get_int_list(data[m], "AniDB ID"), self.library.Plex.language))) + elif method_name == "trakt_list": + self.methods.append((method_name, config.Trakt.validate_trakt_list(util.get_list(data[m])))) + elif method_name == "trakt_watchlist": + self.methods.append((method_name, config.Trakt.validate_trakt_watchlist(util.get_list(data[m]), self.library.is_movie))) + elif method_name == "imdb_list": + new_list = [] + for imdb_list in util.get_list(data[m], split=False): + new_dictionary = {} + if isinstance(imdb_list, dict): + if "url" in imdb_list and imdb_list["url"]: imdb_url = imdb_list["url"] + else: raise Failed("Collection Error: imdb_list attribute url is required") + list_count = util.regex_first_int(imdb_list["limit"], "List Limit", default=0) if "limit" in imdb_list and imdb_list["limit"] else 0 + else: + imdb_url = str(imdb_list) + list_count = 0 + new_list.append({"url": imdb_url, "limit": list_count}) + self.methods.append((method_name, new_list)) + elif method_name in util.dictionary_lists: + if isinstance(data[m], dict): + def get_int(parent, method, data, default, min=1, max=None): + if method not in data: logger.warning("Collection Warning: {} {} attribute not found using {} as default".format(parent, method, default)) + elif not data[method]: logger.warning("Collection Warning: {} {} attribute is blank using {} as default".format(parent, method, default)) + elif isinstance(data[method], int) and data[method] >= min: + if max is None or data[method] <= max: return data[method] + else: logger.warning("Collection Warning: {} {} attribute {} invalid must an integer <= {} using {} as default".format(parent, method, data[method], max, default)) + else: logger.warning("Collection Warning: {} {} attribute {} invalid must an integer >= {} using {} as default".format(parent, method, data[method], min, default)) + return default + if method_name == "filters": + for f in data[m]: + if f in util.method_alias or (f.endswith(".not") and f[:-4] in util.method_alias): + filter = (util.method_alias[f[:-4]] + f[-4:]) if f.endswith(".not") else util.method_alias[f] + logger.warning("Collection Warning: {} filter will run as {}".format(f, filter)) + else: + filter = f + if filter in util.movie_only_filters and self.library.is_show: raise Failed("Collection Error: {} filter only works for movie libraries".format(filter)) + elif data[m][f] is None: raise Failed("Collection Error: {} filter is blank".format(filter)) + elif filter in util.all_filters: self.filters.append((filter, data[m][f])) + else: raise Failed("Collection Error: {} filter not supported".format(filter)) + elif method_name == "plex_collectionless": + new_dictionary = {} + prefix_list = [] + if "exclude_prefix" in data[m] and data[m]["exclude_prefix"]: + if isinstance(data[m]["exclude_prefix"], list): prefix_list.extend(data[m]["exclude_prefix"]) + else: prefix_list.append(str(data[m]["exclude_prefix"])) + exact_list = [] + if "exclude" in data[m] and data[m]["exclude"]: + if isinstance(data[m]["exclude"], list): exact_list.extend(data[m]["exclude"]) + else: exact_list.append(str(data[m]["exclude"])) + if len(prefix_list) == 0 and len(exact_list) == 0: raise Failed("Collection Error: you must have at least one exclusion") + self.details["add_to_arr"] = False + self.details["collection_mode"] = "hide" + self.sync = True + new_dictionary["exclude_prefix"] = prefix_list + new_dictionary["exclude"] = exact_list + self.methods.append((method_name, [new_dictionary])) + elif method_name == "plex_search": + searches = [] + used = [] + for s in 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("Collection Warning: {} plex search attribute will run as {}".format(s, search)) + else: + search = s + if search in util.movie_only_searches and self.library.is_show: + raise Failed("Collection Error: {} plex search attribute only works for movie libraries".format(search)) + elif util.remove_not(search) in used: + raise Failed("Collection Error: Only one instance of {} can be used try using it as a filter instead".format(search)) + elif search in ["year", "year.not"]: + years = util.get_year_list(data[m][s], search) + if len(years) > 0: + used.append(util.remove_not(search)) + searches.append((search, util.get_int_list(data[m][s], util.remove_not(search)))) + elif search in util.plex_searches: + used.append(util.remove_not(search)) + searches.append((search, util.get_list(data[m][s]))) + else: + logger.error("Collection Error: {} plex search attribute not supported".format(search)) + self.methods.append((method_name, [searches])) + elif method_name == "tmdb_discover": + new_dictionary = {"limit": 100} + for attr in data[m]: + if data[m][attr]: + attr_data = data[m][attr] + if (self.library.is_movie and attr in util.discover_movie) or (self.library.is_show and attr in util.discover_tv): + if attr == "language": + if re.compile("([a-z]{2})-([A-Z]{2})").match(str(attr_data)): + new_dictionary[attr] = str(attr_data) + else: + raise Failed("Collection Error: {} attribute {}: {} must match pattern ([a-z]{2})-([A-Z]{2}) e.g. en-US".format(m, attr, attr_data)) + elif attr == "region": + if re.compile("^[A-Z]{2}$").match(str(attr_data)): + new_dictionary[attr] = str(attr_data) + else: + raise Failed("Collection Error: {} attribute {}: {} must match pattern ^[A-Z]{2}$ e.g. US".format(m, attr, attr_data)) + elif attr == "sort_by": + if (self.library.is_movie and attr_data in util.discover_movie_sort) or (self.library.is_show and attr_data in util.discover_tv_sort): + new_dictionary[attr] = attr_data + else: + raise Failed("Collection Error: {} attribute {}: {} is invalid".format(m, attr, attr_data)) + elif attr == "certification_country": + if "certification" in data[m] or "certification.lte" in data[m] or "certification.gte" in data[m]: + new_dictionary[attr] = attr_data + else: + raise Failed("Collection Error: {} attribute {}: must be used with either certification, certification.lte, or certification.gte".format(m, attr)) + elif attr in ["certification", "certification.lte", "certification.gte"]: + if "certification_country" in data[m]: + new_dictionary[attr] = attr_data + else: + raise Failed("Collection Error: {} attribute {}: must be used with certification_country".format(m, attr)) + elif attr in ["include_adult", "include_null_first_air_dates", "screened_theatrically"]: + if attr_data is True: + new_dictionary[attr] = attr_data + elif attr in ["primary_release_date.gte", "primary_release_date.lte", "release_date.gte", "release_date.lte", "air_date.gte", "air_date.lte", "first_air_date.gte", "first_air_date.lte"]: + if re.compile("[0-1]?[0-9][/-][0-3]?[0-9][/-][1-2][890][0-9][0-9]").match(str(attr_data)): + the_date = str(attr_data).split("/") if "/" in str(attr_data) else str(attr_data).split("-") + new_dictionary[attr] = "{}-{}-{}".format(the_date[2], the_date[0], the_date[1]) + elif re.compile("[1-2][890][0-9][0-9][/-][0-1]?[0-9][/-][0-3]?[0-9]").match(str(attr_data)): + the_date = str(attr_data).split("/") if "/" in str(attr_data) else str(attr_data).split("-") + new_dictionary[attr] = "{}-{}-{}".format(the_date[0], the_date[1], the_date[2]) + else: + raise Failed("Collection Error: {} attribute {}: {} must match pattern MM/DD/YYYY e.g. 12/25/2020".format(m, attr, attr_data)) + elif attr in ["primary_release_year", "year", "first_air_date_year"]: + if isinstance(attr_data, int) and 1800 < attr_data and attr_data < 2200: + new_dictionary[attr] = attr_data + else: + raise Failed("Collection Error: {} attribute {}: must be a valid year e.g. 1990".format(m, attr)) + elif attr in ["vote_count.gte", "vote_count.lte", "vote_average.gte", "vote_average.lte", "with_runtime.gte", "with_runtime.lte"]: + if (isinstance(attr_data, int) or isinstance(attr_data, float)) and 0 < attr_data: + new_dictionary[attr] = attr_data + else: + raise Failed("Collection Error: {} attribute {}: must be a valid number greater then 0".format(m, attr)) + elif attr in ["with_cast", "with_crew", "with_people", "with_companies", "with_networks", "with_genres", "without_genres", "with_keywords", "without_keywords", "with_original_language", "timezone"]: + new_dictionary[attr] = attr_data + else: + raise Failed("Collection Error: {} attribute {} not supported".format(m, attr)) + elif attr == "limit": + if isinstance(attr_data, int) and attr_data > 0: + new_dictionary[attr] = attr_data + else: + raise Failed("Collection Error: {} attribute {}: must be a valid number greater then 0".format(m, attr)) + else: + raise Failed("Collection Error: {} attribute {} not supported".format(m, attr)) + else: + raise Failed("Collection Error: {} parameter {} is blank".format(m, attr)) + if len(new_dictionary) > 1: + self.methods.append((method_name, [new_dictionary])) + else: + raise Failed("Collection Error: {} had no valid fields".format(m)) + elif "tautulli" in method_name: + new_dictionary = {} + if method_name == "tautulli_popular": new_dictionary["list_type"] = "popular" + elif method_name == "tautulli_watched": new_dictionary["list_type"] = "watched" + else: raise Failed("Collection Error: {} attribute not supported".format(method_name)) + + new_dictionary["list_days"] = get_int(method_name, "list_days", data[m], 30) + new_dictionary["list_size"] = get_int(method_name, "list_size", data[m], 10) + new_dictionary["list_buffer"] = get_int(method_name, "list_buffer", data[m], 20) + self.methods.append((method_name, [new_dictionary])) + elif method_name == "mal_season": + new_dictionary = {"sort_by": "anime_num_list_users"} + if "sort_by" not in data[m]: logger.warning("Collection Warning: mal_season sort_by attribute not found using members as default") + elif not data[m]["sort_by"]: logger.warning("Collection Warning: mal_season sort_by attribute is blank using members as default") + elif data[m]["sort_by"] not in util.mal_season_sort: logger.warning("Collection Warning: mal_season sort_by attribute {} invalid must be either 'members' or 'score' using members as default".format(data[m]["sort_by"])) + else: new_dictionary["sort_by"] = util.mal_season_sort[data[m]["sort_by"]] + + current_time = datetime.now() + if current_time.month in [1, 2, 3]: new_dictionary["season"] = "winter" + elif current_time.month in [4, 5, 6]: new_dictionary["season"] = "spring" + elif current_time.month in [7, 8, 9]: new_dictionary["season"] = "summer" + elif current_time.month in [10, 11, 12]: new_dictionary["season"] = "fall" + + if "season" not in data[m]: logger.warning("Collection Warning: mal_season season attribute not found using the current season: {} as default".format(new_dictionary["season"])) + elif not data[m]["season"]: logger.warning("Collection Warning: mal_season season attribute is blank using the current season: {} as default".format(new_dictionary["season"])) + elif data[m]["season"] not in util.pretty_seasons: logger.warning("Collection Warning: mal_season season attribute {} invalid must be either 'winter', 'spring', 'summer' or 'fall' using the current season: {} as default".format(data[m]["season"], new_dictionary["season"])) + else: new_dictionary["season"] = data[m]["season"] + + new_dictionary["year"] = get_int(method_name, "year", data[m], current_time.year, min=1917, max=current_time.year + 1) + new_dictionary["limit"] = get_int(method_name, "limit", data[m], 100, max=500) + self.methods.append((method_name, [new_dictionary])) + elif method_name == "mal_userlist": + new_dictionary = {"status": "all", "sort_by": "list_score"} + if "username" not in data[m]: raise Failed("Collection Error: mal_userlist username attribute is required") + elif not data[m]["username"]: raise Failed("Collection Error: mal_userlist username attribute is blank") + else: new_dictionary["username"] = data[m]["username"] + + if "status" not in data[m]: logger.warning("Collection Warning: mal_season status attribute not found using all as default") + elif not data[m]["status"]: logger.warning("Collection Warning: mal_season status attribute is blank using all as default") + elif data[m]["status"] not in util.mal_userlist_status: logger.warning("Collection Warning: mal_season status attribute {} invalid must be either 'all', 'watching', 'completed', 'on_hold', 'dropped' or 'plan_to_watch' using all as default".format(data[m]["status"])) + else: new_dictionary["status"] = util.mal_userlist_status[data[m]["status"]] + + if "sort_by" not in data[m]: logger.warning("Collection Warning: mal_season sort_by attribute not found using score as default") + elif not data[m]["sort_by"]: logger.warning("Collection Warning: mal_season sort_by attribute is blank using score as default") + elif data[m]["sort_by"] not in util.mal_userlist_sort: logger.warning("Collection Warning: mal_season sort_by attribute {} invalid must be either 'score', 'last_updated', 'title' or 'start_date' using score as default".format(data[m]["sort_by"])) + else: new_dictionary["sort_by"] = util.mal_userlist_sort[data[m]["sort_by"]] + + new_dictionary["limit"] = get_int(method_name, "limit", data[m], 100, max=1000) + self.methods.append((method_name, [new_dictionary])) + else: + raise Failed("Collection Error: {} attribute is not a dictionary: {}".format(m, data[m])) + elif method_name in util.count_lists: + list_count = util.regex_first_int(data[m], "List Size", default=20) + if list_count < 1: + logger.warning("Collection Warning: {} must be an integer greater then 0 defaulting to 20".format(method_name)) + list_count = 20 + self.methods.append((method_name, [list_count])) + elif method_name in util.tmdb_lists: + values = config.TMDb.validate_tmdb_list(util.get_int_list(data[m], "TMDb {} ID".format(util.tmdb_type[method_name])), util.tmdb_type[method_name]) + if method_name[-8:] == "_details": + if method_name in ["tmdb_collection_details", "tmdb_movie_details", "tmdb_show_details"]: + item = config.TMDb.get_movie_show_or_collection(values[0], self.library.is_movie) + if "summary" not in self.details and hasattr(item, "overview") and item.overview: + self.details["summary"] = item.overview + if "background" not in self.details and hasattr(item, "backdrop_path") and item.backdrop_path: + self.details["background"] = ("url", "{}{}".format(config.TMDb.image_url, item.backdrop_path), method_name[:-8]) + if "poster" not in self.details and hasattr(item, "poster_path") and item.poster_path: + self.details["poster"] = ("url", "{}{}".format(config.TMDb.image_url, item.poster_path), method_name[:-8]) + else: + item = config.TMDb.get_list(values[0]) + if "summary" not in self.details and hasattr(item, "description") and item.description: + self.details["summary"] = item.description + self.methods.append((method_name[:-8], values)) + else: + self.methods.append((method_name, values)) + elif method_name in util.all_lists: + self.methods.append((method_name, util.get_list(data[m]))) + elif method_name not in util.other_attributes: + raise Failed("Collection Error: {} attribute not supported".format(method_name)) + else: + raise Failed("Collection Error: {} attribute is blank".format(m)) + + self.do_arr = False + if self.library.Radarr: + self.do_arr = self.details["add_to_arr"] if "add_to_arr" in self.details else self.library.Radarr.add + if self.library.Sonarr: + self.do_arr = self.details["add_to_arr"] if "add_to_arr" in self.details else self.library.Sonarr.add + + def run_methods(self, collection_obj, collection_name, map, movie_map, show_map): + items_found = 0 + for method, values in self.methods: + logger.debug("") + logger.debug("Method: {}".format(method)) + logger.debug("Values: {}".format(values)) + pretty = util.pretty_names[method] if method in util.pretty_names else method + for value in values: + items = [] + missing_movies = [] + missing_shows = [] + def check_map(input_ids): + movie_ids, show_ids = input_ids + items_found_inside = 0 + if len(movie_ids) > 0: + items_found_inside += len(movie_ids) + for movie_id in movie_ids: + if movie_id in movie_map: items.append(movie_map[movie_id]) + else: missing_movies.append(movie_id) + if len(show_ids) > 0: + items_found_inside += len(show_ids) + for show_id in show_ids: + if show_id in show_map: items.append(show_map[show_id]) + else: missing_shows.append(show_id) + return items_found_inside + logger.info("") + logger.debug("Value: {}".format(value)) + if method == "plex_all": + logger.info("Processing {} {}".format(pretty, "Movies" if self.library.is_movie else "Shows")) + items = self.library.Plex.all() + items_found += len(items) + elif method == "plex_collection": + items = value.items() + items_found += len(items) + elif method == "plex_search": + search_terms = {} + output = "" + for i, attr_pair in enumerate(value): + search_list = attr_pair[1] + final_method = attr_pair[0][:-4] + "!" if attr_pair[0][-4:] == ".not" else attr_pair[0] + if self.library.is_show: + final_method = "show." + final_method + search_terms[final_method] = search_list + ors = "" + for o, param in enumerate(attr_pair[1]): + ors += "{}{}".format(" OR " if o > 0 else "{}(".format(attr_pair[0]), param) + logger.info("\t\t AND {})".format(ors) if i > 0 else "Processing {}: {})".format(pretty, ors)) + items = self.library.Plex.search(**search_terms) + items_found += len(items) + elif method == "plex_collectionless": + good_collections = [] + for col in self.library.get_all_collections(): + keep_collection = True + for pre in value["exclude_prefix"]: + if col.title.startswith(pre) or (col.titleSort and col.titleSort.startswith(pre)): + keep_collection = False + break + for ext in value["exclude"]: + if col.title == ext or (col.titleSort and col.titleSort == ext): + keep_collection = False + break + if keep_collection: + good_collections.append(col.title.lower()) + + all_items = self.library.Plex.all() + length = 0 + for i, item in enumerate(all_items, 1): + length = util.print_return(length, "Processing: {}/{} {}".format(i, len(all_items), item.title)) + add_item = True + for collection in item.collections: + if collection.tag.lower() in good_collections: + add_item = False + break + if add_item: + items.append(item) + items_found += len(items) + util.print_end(length, "Processed {} {}".format(len(all_items), "Movies" if self.library.is_movie else "Shows")) + elif "tautulli" in method: + items = self.library.Tautulli.get_items(self.library, time_range=value["list_days"], stats_count=value["list_size"], list_type=value["list_type"], stats_count_buffer=value["list_buffer"]) + items_found += len(items) + elif "anidb" in method: items_found += check_map(self.config.AniDB.get_items(method, value, self.library.Plex.language)) + elif "mal" in method: items_found += check_map(self.config.MyAnimeList.get_items(method, value)) + elif "tvdb" in method: items_found += check_map(self.config.TVDb.get_items(method, value, self.library.Plex.language)) + elif "imdb" in method: items_found += check_map(self.config.IMDb.get_items(method, value, self.library.Plex.language)) + elif "tmdb" in method: items_found += check_map(self.config.TMDb.get_items(method, value, self.library.is_movie)) + elif "trakt" in method: items_found += check_map(self.config.Trakt.get_items(method, value, self.library.is_movie)) + else: logger.error("Collection Error: {} method not supported".format(method)) + + if len(items) > 0: map = self.library.add_to_collection(collection_obj if collection_obj else collection_name, items, self.filters, self.library.show_filtered, map, movie_map, show_map) + else: logger.error("No items found to add to this collection ") + + if len(missing_movies) > 0 or len(missing_shows) > 0: + logger.info("") + if len(missing_movies) > 0: + not_lang = None + terms = None + for filter_method, filter_data in self.filters: + if filter_method.startswith("original_language"): + terms = util.get_list(filter_data, lower=True) + not_lang = filter_method.endswith(".not") + break + + missing_movies_with_names = [] + for missing_id in missing_movies: + try: + movie = self.config.TMDb.get_movie(missing_id) + title = str(movie.title) + if not_lang is None or (not_lang is True and movie.original_language not in terms) or (not_lang is False and movie.original_language in terms): + missing_movies_with_names.append((title, missing_id)) + logger.info("{} Collection | ? | {} (TMDb: {})".format(collection_name, title, missing_id)) + elif self.library.show_filtered is True: + logger.info("{} Collection | X | {} (TMDb: {})".format(collection_name, title, missing_id)) + except Failed as e: + logger.error(e) + logger.info("{} Movie{} Missing".format(len(missing_movies_with_names), "s" if len(missing_movies_with_names) > 1 else "")) + self.library.add_missing(collection_name, missing_movies_with_names, True) + if self.do_arr and self.library.Radarr: + self.library.Radarr.add_tmdb([missing_id for title, missing_id in missing_movies_with_names], tag=self.details["arr_tag"]) + if len(missing_shows) > 0 and self.library.is_show: + missing_shows_with_names = [] + for missing_id in missing_shows: + try: + title = str(self.config.TVDb.get_series(self.library.Plex.language, tvdb_id=missing_id).title.encode("ascii", "replace").decode()) + missing_shows_with_names.append((title, missing_id)) + logger.info("{} Collection | ? | {} (TVDB: {})".format(collection_name, title, missing_id)) + except Failed as e: + logger.error(e) + logger.info("{} Show{} Missing".format(len(missing_shows_with_names), "s" if len(missing_shows_with_names) > 1 else "")) + self.library.add_missing(c, missing_shows_with_names, False) + if self.do_arr and self.library.Sonarr: + self.library.Sonarr.add_tvdb([missing_id for title, missing_id in missing_shows_with_names], tag=self.details["arr_tag"]) + + if self.sync and items_found > 0: + logger.info("") + count_removed = 0 + for ratingKey, item in map.items(): + if item is not None: + logger.info("{} Collection | - | {}".format(collection_name, item.title)) + item.removeCollection(collection_name) + count_removed += 1 + logger.info("{} {}{} Removed".format(count_removed, "Movie" if self.library.is_movie else "Show", "s" if count_removed == 1 else "")) + logger.info("") + + def update_details(self, collection): + edits = {} + if "sort_title" in self.details: + edits["titleSort.value"] = self.details["sort_title"] + edits["titleSort.locked"] = 1 + if "content_rating" in self.details: + edits["contentRating.value"] = self.details["content_rating"] + edits["contentRating.locked"] = 1 + if "summary" in self.details: + edits["summary.value"] = self.details["summary"] + edits["summary.locked"] = 1 + if len(edits) > 0: + logger.debug(edits) + collection.edit(**edits) + collection.reload() + logger.info("Details: have been updated") + if "collection_mode" in self.details: + collection.modeUpdate(mode=self.details["collection_mode"]) + if "collection_order" in self.details: + collection.sortUpdate(sort=self.details["collection_order"]) + + if self.library.asset_directory: + name_mapping = self.name + if "name_mapping" in self.details: + if self.details["name_mapping"]: name_mapping = self.details["name_mapping"] + else: logger.error("Collection Error: name_mapping attribute is blank") + path = os.path.join(self.library.asset_directory, "{}".format(name_mapping), "poster.*") + matches = glob.glob(path) + if len(matches) > 0: + for match in matches: + self.posters.append(("file", os.path.abspath(match), "asset_directory")) + elif len(self.posters) == 0 and "poster" not in self.details: + logger.warning("poster not found at: {}".format(os.path.abspath(path))) + path = os.path.join(self.library.asset_directory, "{}".format(name_mapping), "background.*") + matches = glob.glob(path) + if len(matches) > 0: + for match in matches: + self.backgrounds.append(("file", os.path.abspath(match), "asset_directory")) + elif len(self.backgrounds) == 0 and "background" not in self.details: + logger.warning("background not found at: {}".format(os.path.abspath(path))) + + poster = util.choose_from_list(self.posters, "poster", list_type="tuple") + if not poster and "poster" in self.details: poster = self.details["poster"] + if poster: + if poster[0] == "url": collection.uploadPoster(url=poster[1]) + else: collection.uploadPoster(filepath=poster[1]) + logger.info("Detail: {} updated poster to [{}] {}".format(poster[2], poster[0], poster[1])) + + background = util.choose_from_list(self.backgrounds, "background", list_type="tuple") + if not background and "background" in self.details: background = self.details["background"] + if background: + if background[0] == "url": collection.uploadArt(url=background[1]) + else: collection.uploadArt(filepath=background[1]) + logger.info("Detail: {} updated background to [{}] {}".format(background[2], background[0], background[1])) + + if self.library.asset_directory: + path = os.path.join(self.library.asset_directory, "{}".format(name_mapping)) + if os.path.isdir(path): + dirs = [folder for folder in os.listdir(path) if os.path.isdir(os.path.join(path, folder))] + if len(dirs) > 0: + for item in collection.items(): + folder = os.path.basename(os.path.dirname(item.locations[0])) + if folder in dirs: + files = [file for file in os.listdir(os.path.join(path, folder)) if os.path.isfile(os.path.join(path, folder, file))] + poster_path = None + background_path = None + for file in files: + if poster_path is None and file.startswith("poster."): + poster_path = os.path.join(path, folder, file) + if background_path is None and file.startswith("background."): + background_path = os.path.join(path, folder, file) + if poster_path: + item.uploadPoster(filepath=poster_path) + logger.info("Detail: asset_directory updated {}'s poster to [file] {}".format(item.title, poster_path)) + if background_path: + item.uploadArt(filepath=background_path) + logger.info("Detail: asset_directory updated {}'s background to [file] {}".format(item.title, background_path)) + if poster_path is None and background_path is None: + logger.warning("No Files Found: {}".format(os.path.join(path, folder))) + else: + logger.warning("No Folder: {}".format(os.path.join(path, folder))) diff --git a/modules/config.py b/modules/config.py index afda33b2..01074862 100644 --- a/modules/config.py +++ b/modules/config.py @@ -1,7 +1,7 @@ -import glob, json, logging, os, re, requests -from datetime import datetime, timedelta +import glob, logging, os, re, requests from modules import util from modules.anidb import AniDBAPI +from modules.builder import CollectionBuilder from modules.cache import Cache from modules.imdb import IMDbAPI from modules.plex import PlexAPI @@ -44,7 +44,9 @@ class Config: elif attribute not in new_config[parent]: new_config[parent][attribute] = default else: endLine = "" yaml.round_trip_dump(new_config, open(self.config_path, "w"), indent=ind, block_seq_indent=bsi) - elif not data[attribute] and data[attribute] != False: message = "Config Error: {} is blank".format(text) + elif not data[attribute] and data[attribute] != False: + if default_is_none is True: return None + else: message = "Config Error: {} is blank".format(text) elif var_type == "bool": if isinstance(data[attribute], bool): return data[attribute] else: message = "Config Error: {} must be either true or false".format(text) @@ -138,6 +140,10 @@ class Config: self.IMDb = IMDbAPI(Cache=self.Cache, TMDb=self.TMDb, Trakt=self.Trakt, TVDb=self.TVDb) if self.TMDb or self.Trakt else None self.AniDB = AniDBAPI(Cache=self.Cache, TMDb=self.TMDb, Trakt=self.Trakt) + util.seperator() + + logger.info("Connecting to Plex Libraries...") + self.general["plex"] = {} self.general["plex"]["url"] = check_for_attribute(self.data, "url", parent="plex", default_is_none=True) if "plex" in self.data else None self.general["plex"]["token"] = check_for_attribute(self.data, "token", parent="plex", default_is_none=True) if "plex" in self.data else None @@ -170,9 +176,6 @@ class Config: self.general["tautulli"]["url"] = check_for_attribute(self.data, "url", parent="tautulli", default_is_none=True) if "tautulli" in self.data else None self.general["tautulli"]["apikey"] = check_for_attribute(self.data, "apikey", parent="tautulli", default_is_none=True) if "tautulli" in self.data else None - util.seperator() - - logger.info("Connecting to Plex Libraries...") self.libraries = [] libs = check_for_attribute(self.data, "libraries", throw=True) @@ -449,71 +452,11 @@ class Config: logger.info("") map = {} - details = {} - methods = [] - filters = [] - posters_found = [] - backgrounds_found = [] - collectionless = "plex_collectionless" in collections[c] - skip_collection = True - show_filtered = library.show_filtered - - if "schedule" not in collections[c]: - skip_collection = False - elif not collections[c]["schedule"]: - logger.error("Collection Error: schedule attribute is blank. Running daily") - skip_collection = False - else: - schedule_list = util.get_list(collections[c]["schedule"]) - current_time = datetime.now() - next_month = current_time.replace(day=28) + timedelta(days=4) - last_day = next_month - timedelta(days=next_month.day) - for schedule in schedule_list: - run_time = str(schedule).lower() - if run_time.startswith("day") or run_time.startswith("daily"): - skip_collection = False - break - if run_time.startswith("week") or run_time.startswith("month") or run_time.startswith("year"): - match = re.search("\\(([^)]+)\\)", run_time) - if match: - param = match.group(1) - if run_time.startswith("week"): - if param.lower() in util.days_alias: - weekday = util.days_alias[param.lower()] - logger.info("Scheduled weekly on {}".format(util.pretty_days[weekday])) - if weekday == current_time.weekday(): - skip_collection = False - break - else: - logger.error("Collection Error: weekly schedule attribute {} invalid must be a day of the weeek i.e. weekly(Monday)".format(schedule)) - elif run_time.startswith("month"): - try: - if 1 <= int(param) <= 31: - logger.info("Scheduled monthly on the {}".format(util.make_ordinal(param))) - if current_time.day == int(param) or (current_time.day == last_day.day and int(param) > last_day.day): - skip_collection = False - break - else: - logger.error("Collection Error: monthly schedule attribute {} invalid must be between 1 and 31".format(schedule)) - except ValueError: - logger.error("Collection Error: monthly schedule attribute {} invalid must be an integer".format(schedule)) - elif run_time.startswith("year"): - match = re.match("^(1[0-2]|0?[1-9])/(3[01]|[12][0-9]|0?[1-9])$", param) - if match: - month = int(match.group(1)) - day = int(match.group(2)) - logger.info("Scheduled yearly on {} {}".format(util.pretty_months[month], util.make_ordinal(day))) - if current_time.month == month and (current_time.day == day or (current_time.day == last_day.day and day > last_day.day)): - skip_collection = False - break - else: - logger.error("Collection Error: yearly schedule attribute {} invalid must be in the MM/DD format i.e. yearly(11/22)".format(schedule)) - else: - logger.error("Collection Error: failed to parse schedule: {}".format(schedule)) - else: - logger.error("Collection Error: schedule attribute {} invalid".format(schedule)) - if skip_collection: - logger.info("Skipping Collection {}".format(c)) + try: + builder = CollectionBuilder(self, library, c, collections[c]) + except Exception as e: + util.print_stacktrace() + logger.error(e) continue try: @@ -523,12 +466,11 @@ class Config: collection_obj = None collection_name = c - sync_collection = library.sync_mode == "sync" - if "sync_mode" in collections[c]: - if not collections[c]["sync_mode"]: logger.warning("Collection Warning: sync_mode attribute is blank using general: {}".format(library.sync_mode)) - elif collections[c]["sync_mode"] not in ["append", "sync"]: logger.warning("Collection Warning: {} sync_mode invalid using general: {}".format(library.sync_mode, collections[c]["sync_mode"])) - else: sync_collection = collections[c]["sync_mode"] == "sync" - if sync_collection or collectionless: + if builder.schedule is not None: + print_multiline(builder.schedule, info=True) + + logger.info("") + if builder.sync: logger.info("Sync Mode: sync") if collection_obj: for item in collection_obj.items(): @@ -536,501 +478,12 @@ class Config: else: logger.info("Sync Mode: append") - if "tmdb_person" in collections[c]: - if collections[c]["tmdb_person"]: - valid_names = [] - for tmdb_id in util.get_int_list(collections[c]["tmdb_person"], "TMDb Person ID"): - try: - person = self.TMDb.get_person(tmdb_id) - valid_names.append(person.name) - if "summary" not in details and hasattr(person, "biography") and person.biography: - details["summary"] = person.biography - if "poster" not in details and hasattr(person, "profile_path") and person.profile_path: - details["poster"] = ("url", "{}{}".format(self.TMDb.image_url, person.profile_path), "tmdb_person") - except Failed as e: - util.print_stacktrace() - logger.error(e) - if len(valid_names) > 0: details["tmdb_person"] = valid_names - else: logger.error("Collection Error: No valid TMDb Person IDs in {}".format(collections[c]["tmdb_person"])) - else: - logger.error("Collection Error: tmdb_person attribute is blank") - - for m in collections[c]: - try: - if "tmdb" in m and not self.TMDb: - logger.info("Collection Error: {} skipped. TMDb must be configured".format(m)) - map = {} - elif "trakt" in m and not self.Trakt: - logger.info("Collection Error: {} skipped. Trakt must be configured".format(m)) - map = {} - elif "imdb" in m and not self.IMDb: - logger.info("Collection Error: {} skipped. TMDb or Trakt must be configured".format(m)) - map = {} - elif "tautulli" in m and not library.Tautulli: - logger.info("Collection Error: {} skipped. Tautulli must be configured".format(m)) - map = {} - elif "mal" in m and not self.MyAnimeList: - logger.info("Collection Error: {} skipped. MyAnimeList must be configured".format(m)) - map = {} - elif collections[c][m] is not None: - logger.debug("") - logger.debug("Method: {}".format(m)) - logger.debug("Value: {}".format(collections[c][m])) - if m in util.method_alias: - method_name = util.method_alias[m] - logger.warning("Collection Warning: {} attribute will run as {}".format(m, method_name)) - else: - method_name = m - if method_name in util.show_only_lists and library.is_movie: raise Failed("Collection Error: {} attribute only works for show libraries".format(method_name)) - elif method_name in util.movie_only_lists and library.is_show: raise Failed("Collection Error: {} attribute only works for movie libraries".format(method_name)) - elif method_name in util.movie_only_searches and library.is_show: raise Failed("Collection Error: {} plex search only works for movie libraries".format(method_name)) - elif method_name not in util.collectionless_lists and collectionless: raise Failed("Collection Error: {} attribute does not work for Collectionless collection".format(method_name)) - elif method_name == "tmdb_summary": details["summary"] = self.TMDb.get_movie_show_or_collection(util.regex_first_int(collections[c][m], "TMDb ID"), library.is_movie).overview - elif method_name == "tmdb_description": details["summary"] = self.TMDb.get_list(util.regex_first_int(collections[c][m], "TMDb List ID")).description - elif method_name == "tmdb_biography": details["summary"] = self.TMDb.get_person(util.regex_first_int(collections[c][m], "TMDb Person ID")).biography - elif method_name == "collection_mode": - if collections[c][m] in ["default", "hide", "hide_items", "show_items", "hideItems", "showItems"]: - if collections[c][m] == "hide_items": details[method_name] = "hideItems" - elif collections[c][m] == "show_items": details[method_name] = "showItems" - else: details[method_name] = collections[c][m] - else: raise Failed("Collection Error: {} collection_mode Invalid\n| \tdefault (Library default)\n| \thide (Hide Collection)\n| \thide_items (Hide Items in this Collection)\n| \tshow_items (Show this Collection and its Items)".format(collections[c][m])) - elif method_name == "collection_order": - if collections[c][m] in ["release", "alpha"]: details[method_name] = collections[c][m] - else: raise Failed("Collection Error: {} collection_order Invalid\n| \trelease (Order Collection by release dates)\n| \talpha (Order Collection Alphabetically)".format(collections[c][m])) - elif method_name == "url_poster": posters_found.append(("url", collections[c][m], method_name)) - elif method_name == "tmdb_poster": posters_found.append(("url", "{}{}".format(self.TMDb.image_url, self.TMDb.get_movie_show_or_collection(util.regex_first_int(collections[c][m], "TMDb ID"), library.is_movie).poster_path), method_name)) - elif method_name == "tmdb_profile": posters_found.append(("url", "{}{}".format(self.TMDb.image_url, self.TMDb.get_person(util.regex_first_int(collections[c][m], "TMDb Person ID")).profile_path), method_name)) - elif method_name == "file_poster": - if os.path.exists(collections[c][m]): posters_found.append(("file", os.path.abspath(collections[c][m]), method_name)) - else: raise Failed("Collection Error: Poster Path Does Not Exist: {}".format(os.path.abspath(collections[c][m]))) - elif method_name == "url_background": backgrounds_found.append(("url", collections[c][m], method_name)) - elif method_name == "tmdb_background": backgrounds_found.append(("url", "{}{}".format(self.TMDb.image_url, self.TMDb.get_movie_show_or_collection(util.regex_first_int(collections[c][m], "TMDb ID"), library.is_movie).poster_path), method_name)) - elif method_name == "file_background": - if os.path.exists(collections[c][m]): backgrounds_found.append(("file", os.path.abspath(collections[c][m]), method_name)) - else: raise Failed("Collection Error: Background Path Does Not Exist: {}".format(os.path.abspath(collections[c][m]))) - elif method_name == "add_to_arr": - if isinstance(collections[c][m], bool): details[method_name] = collections[c][m] - else: raise Failed("Collection Error: add_to_arr must be either true or false") - elif method_name == "arr_tag": details[method_name] = util.get_list(collections[c][m]) - elif method_name == "show_filtered": - if isinstance(collections[c][m], bool): show_filtered = collections[c][m] - else: raise Failed("Collection Error: show_filtered must be either true or false using the default false") - elif method_name in util.all_details: details[method_name] = collections[c][m] - elif method_name in ["year", "year.not"]: methods.append(("plex_search", [[(method_name, util.get_year_list(collections[c][m], method_name))]])) - elif method_name in ["decade", "decade.not"]: methods.append(("plex_search", [[(method_name, util.get_int_list(collections[c][m], util.remove_not(method_name)))]])) - elif method_name in util.tmdb_searches: - final_values = [] - for value in util.get_list(collections[c][m]): - if value.lower() == "tmdb" and "tmdb_person" in details: - for name in details["tmdb_person"]: - final_values.append(name) - else: - final_values.append(value) - methods.append(("plex_search", [[(method_name, final_values)]])) - elif method_name in util.plex_searches: methods.append(("plex_search", [[(method_name, util.get_list(collections[c][m]))]])) - elif method_name == "plex_all": methods.append((method_name, [""])) - elif method_name == "plex_collection": methods.append((method_name, library.validate_collections(collections[c][m] if isinstance(collections[c][m], list) else [collections[c][m]]))) - elif method_name == "anidb_popular": - list_count = util.regex_first_int(collections[c][m], "List Size", default=40) - if 1 <= list_count <= 30: - methods.append((method_name, [list_count])) - else: - logger.error("Collection Error: anidb_popular must be an integer between 1 and 30 defaulting to 30") - methods.append((method_name, [30])) - elif method_name == "mal_id": methods.append((method_name, util.get_int_list(collections[c][m], "MyAnimeList ID"))) - elif method_name in ["anidb_id", "anidb_relation"]: methods.append((method_name, self.AniDB.validate_anidb_list(util.get_int_list(collections[c][m], "AniDB ID"), library.Plex.language))) - elif method_name == "trakt_list": methods.append((method_name, self.Trakt.validate_trakt_list(util.get_list(collections[c][m])))) - elif method_name == "trakt_watchlist": methods.append((method_name, self.Trakt.validate_trakt_watchlist(util.get_list(collections[c][m]), library.is_movie))) - elif method_name == "imdb_list": - new_list = [] - for imdb_list in util.get_list(collections[c][m], split=False): - new_dictionary = {} - if isinstance(imdb_list, dict): - if "url" in imdb_list and imdb_list["url"]: imdb_url = imdb_list["url"] - else: raise Failed("Collection Error: imdb_list attribute url is required") - list_count = util.regex_first_int(imdb_list["limit"], "List Limit", default=0) if "limit" in imdb_list and imdb_list["limit"] else 0 - else: - imdb_url = str(imdb_list) - list_count = 0 - new_list.append({"url": imdb_url, "limit": list_count}) - methods.append((method_name, new_list)) - elif method_name in util.dictionary_lists: - if isinstance(collections[c][m], dict): - def get_int(parent, method, data, default, min=1, max=None): - if method not in data: logger.warning("Collection Warning: {} {} attribute not found using {} as default".format(parent, method, default)) - elif not data[method]: logger.warning("Collection Warning: {} {} attribute is blank using {} as default".format(parent, method, default)) - elif isinstance(data[method], int) and data[method] >= min: - if max is None or data[method] <= max: return data[method] - else: logger.warning("Collection Warning: {} {} attribute {} invalid must an integer <= {} using {} as default".format(parent, method, data[method], max, default)) - else: logger.warning("Collection Warning: {} {} attribute {} invalid must an integer >= {} using {} as default".format(parent, method, data[method], min, default)) - return default - if method_name == "filters": - for filter in collections[c][m]: - if filter in util.method_alias or (filter.endswith(".not") and filter[:-4] in util.method_alias): - final_filter = (util.method_alias[filter[:-4]] + filter[-4:]) if filter.endswith(".not") else util.method_alias[filter] - logger.warning("Collection Warning: {} filter will run as {}".format(filter, final_filter)) - else: - final_filter = filter - if final_filter in util.movie_only_filters and library.is_show: - logger.error("Collection Error: {} filter only works for movie libraries".format(final_filter)) - elif collections[c][m][filter] is None: - logger.error("Collection Error: {} filter is blank".format(final_filter)) - elif final_filter in util.all_filters: - filters.append((final_filter, collections[c][m][filter])) - else: - logger.error("Collection Error: {} filter not supported".format(final_filter)) - elif method_name == "plex_collectionless": - new_dictionary = {} - - prefix_list = [] - if "exclude_prefix" in collections[c][m] and collections[c][m]["exclude_prefix"]: - if isinstance(collections[c][m]["exclude_prefix"], list): - prefix_list.extend(collections[c][m]["exclude_prefix"]) - else: - prefix_list.append("{}".format(collections[c][m]["exclude_prefix"])) - - exact_list = [] - if "exclude" in collections[c][m] and collections[c][m]["exclude"]: - if isinstance(collections[c][m]["exclude"], list): - exact_list.extend(collections[c][m]["exclude"]) - else: - exact_list.append("{}".format(collections[c][m]["exclude"])) - - if len(prefix_list) == 0 and len(exact_list) == 0: - raise Failed("Collection Error: you must have at least one exclusion") - details["add_to_arr"] = False - details["collection_mode"] = "hide" - new_dictionary["exclude_prefix"] = prefix_list - new_dictionary["exclude"] = exact_list - methods.append((method_name, [new_dictionary])) - elif method_name == "plex_search": - search = [] - searches_used = [] - for search_attr in collections[c][m]: - if search_attr in util.method_alias or (search_attr.endswith(".not") and search_attr[:-4] in util.method_alias): - final_attr = (util.method_alias[search_attr[:-4]] + search_attr[-4:]) if search_attr.endswith(".not") else util.method_alias[search_attr] - logger.warning("Collection Warning: {} plex search attribute will run as {}".format(search_attr, final_attr)) - else: - final_attr = search_attr - if final_attr in util.movie_only_searches and library.is_show: - logger.error("Collection Error: {} plex search attribute only works for movie libraries".format(final_attr)) - elif util.remove_not(final_attr) in searches_used: - logger.error("Collection Error: Only one instance of {} can be used try using it as a filter instead".format(final_attr)) - elif final_attr in ["year", "year.not"]: - years = util.get_year_list(collections[c][m][search_attr], final_attr) - if len(years) > 0: - searches_used.append(util.remove_not(final_attr)) - search.append((final_attr, util.get_int_list(collections[c][m][search_attr], util.remove_not(final_attr)))) - elif final_attr in util.plex_searches: - searches_used.append(util.remove_not(final_attr)) - search.append((final_attr, util.get_list(collections[c][m][search_attr]))) - else: - logger.error("Collection Error: {} plex search attribute not supported".format(search_attr)) - methods.append((method_name, [search])) - elif method_name == "tmdb_discover": - new_dictionary = {"limit": 100} - for attr in collections[c][m]: - if collections[c][m][attr]: - attr_data = collections[c][m][attr] - if (library.is_movie and attr in util.discover_movie) or (library.is_show and attr in util.discover_tv): - if attr == "language": - if re.compile("([a-z]{2})-([A-Z]{2})").match(str(attr_data)): - new_dictionary[attr] = str(attr_data) - else: - logger.error("Collection Error: Skipping {} attribute {}: {} must match pattern ([a-z]{2})-([A-Z]{2}) e.g. en-US".format(m, attr, attr_data)) - elif attr == "region": - if re.compile("^[A-Z]{2}$").match(str(attr_data)): - new_dictionary[attr] = str(attr_data) - else: - logger.error("Collection Error: Skipping {} attribute {}: {} must match pattern ^[A-Z]{2}$ e.g. US".format(m, attr, attr_data)) - elif attr == "sort_by": - if (library.is_movie and attr_data in util.discover_movie_sort) or (library.is_show and attr_data in util.discover_tv_sort): - new_dictionary[attr] = attr_data - else: - logger.error("Collection Error: Skipping {} attribute {}: {} is invalid".format(m, attr, attr_data)) - elif attr == "certification_country": - if "certification" in collections[c][m] or "certification.lte" in collections[c][m] or "certification.gte" in collections[c][m]: - new_dictionary[attr] = attr_data - else: - logger.error("Collection Error: Skipping {} attribute {}: must be used with either certification, certification.lte, or certification.gte".format(m, attr)) - elif attr in ["certification", "certification.lte", "certification.gte"]: - if "certification_country" in collections[c][m]: - new_dictionary[attr] = attr_data - else: - logger.error("Collection Error: Skipping {} attribute {}: must be used with certification_country".format(m, attr)) - elif attr in ["include_adult", "include_null_first_air_dates", "screened_theatrically"]: - if attr_data is True: - new_dictionary[attr] = attr_data - elif attr in ["primary_release_date.gte", "primary_release_date.lte", "release_date.gte", "release_date.lte", "air_date.gte", "air_date.lte", "first_air_date.gte", "first_air_date.lte"]: - if re.compile("[0-1]?[0-9][/-][0-3]?[0-9][/-][1-2][890][0-9][0-9]").match(str(attr_data)): - the_date = str(attr_data).split("/") if "/" in str(attr_data) else str(attr_data).split("-") - new_dictionary[attr] = "{}-{}-{}".format(the_date[2], the_date[0], the_date[1]) - elif re.compile("[1-2][890][0-9][0-9][/-][0-1]?[0-9][/-][0-3]?[0-9]").match(str(attr_data)): - the_date = str(attr_data).split("/") if "/" in str(attr_data) else str(attr_data).split("-") - new_dictionary[attr] = "{}-{}-{}".format(the_date[0], the_date[1], the_date[2]) - else: - logger.error("Collection Error: Skipping {} attribute {}: {} must match pattern MM/DD/YYYY e.g. 12/25/2020".format(m, attr, attr_data)) - elif attr in ["primary_release_year", "year", "first_air_date_year"]: - if isinstance(attr_data, int) and 1800 < attr_data and attr_data < 2200: - new_dictionary[attr] = attr_data - else: - logger.error("Collection Error: Skipping {} attribute {}: must be a valid year e.g. 1990".format(m, attr)) - elif attr in ["vote_count.gte", "vote_count.lte", "vote_average.gte", "vote_average.lte", "with_runtime.gte", "with_runtime.lte"]: - if (isinstance(attr_data, int) or isinstance(attr_data, float)) and 0 < attr_data: - new_dictionary[attr] = attr_data - else: - logger.error("Collection Error: Skipping {} attribute {}: must be a valid number greater then 0".format(m, attr)) - elif attr in ["with_cast", "with_crew", "with_people", "with_companies", "with_networks", "with_genres", "without_genres", "with_keywords", "without_keywords", "with_original_language", "timezone"]: - new_dictionary[attr] = attr_data - else: - logger.error("Collection Error: {} attribute {} not supported".format(m, attr)) - elif attr == "limit": - if isinstance(attr_data, int) and attr_data > 0: - new_dictionary[attr] = attr_data - else: - logger.error("Collection Error: Skipping {} attribute {}: must be a valid number greater then 0".format(m, attr)) - else: - logger.error("Collection Error: {} attribute {} not supported".format(m, attr)) - else: - logger.error("Collection Error: {} parameter {} is blank".format(m, attr)) - if len(new_dictionary) > 1: - methods.append((method_name, [new_dictionary])) - else: - logger.error("Collection Error: {} had no valid fields".format(m)) - elif "tautulli" in method_name: - new_dictionary = {} - if method_name == "tautulli_popular": new_dictionary["list_type"] = "popular" - elif method_name == "tautulli_watched": new_dictionary["list_type"] = "watched" - else: raise Failed("Collection Error: {} attribute not supported".format(method_name)) - new_dictionary["list_days"] = get_int(method_name, "list_days", collections[c][m], 30) - new_dictionary["list_size"] = get_int(method_name, "list_size", collections[c][m], 10) - new_dictionary["list_buffer"] = get_int(method_name, "list_buffer", collections[c][m], 20) - methods.append((method_name, [new_dictionary])) - elif method_name == "mal_season": - new_dictionary = {"sort_by": "anime_num_list_users"} - current_time = datetime.now() - if current_time.month in [1, 2, 3]: new_dictionary["season"] = "winter" - elif current_time.month in [4, 5, 6]: new_dictionary["season"] = "spring" - elif current_time.month in [7, 8, 9]: new_dictionary["season"] = "summer" - elif current_time.month in [10, 11, 12]: new_dictionary["season"] = "fall" - new_dictionary["year"] = get_int(method_name, "year", collections[c][m], current_time.year, min=1917, max=current_time.year + 1) - new_dictionary["limit"] = get_int(method_name, "limit", collections[c][m], 100, max=500) - if "sort_by" not in collections[c][m]: logger.warning("Collection Warning: mal_season sort_by attribute not found using members as default") - elif not collections[c][m]["sort_by"]: logger.warning("Collection Warning: mal_season sort_by attribute is blank using members as default") - elif collections[c][m]["sort_by"] not in util.mal_season_sort: logger.warning("Collection Warning: mal_season sort_by attribute {} invalid must be either 'members' or 'score' using members as default".format(collections[c][m]["sort_by"])) - else: new_dictionary["sort_by"] = util.mal_season_sort[collections[c][m]["sort_by"]] - if "season" not in collections[c][m]: logger.warning("Collection Warning: mal_season season attribute not found using the current season: {} as default".format(new_dictionary["season"])) - elif not collections[c][m]["season"]: logger.warning("Collection Warning: mal_season season attribute is blank using the current season: {} as default".format(new_dictionary["season"])) - elif collections[c][m]["season"] not in util.pretty_seasons: logger.warning("Collection Warning: mal_season season attribute {} invalid must be either 'winter', 'spring', 'summer' or 'fall' using the current season: {} as default".format(collections[c][m]["season"], new_dictionary["season"])) - else: new_dictionary["season"] = collections[c][m]["season"] - methods.append((method_name, [new_dictionary])) - elif method_name == "mal_userlist": - new_dictionary = {"status": "all", "sort_by": "list_score"} - if "username" not in collections[c][m]: raise Failed("Collection Error: mal_userlist username attribute is required") - elif not collections[c][m]["username"]: raise Failed("Collection Error: mal_userlist username attribute is blank") - else: new_dictionary["username"] = collections[c][m]["username"] - if "status" not in collections[c][m]: logger.warning("Collection Warning: mal_season status attribute not found using all as default") - elif not collections[c][m]["status"]: logger.warning("Collection Warning: mal_season status attribute is blank using all as default") - elif collections[c][m]["status"] not in util.mal_userlist_status: logger.warning("Collection Warning: mal_season status attribute {} invalid must be either 'all', 'watching', 'completed', 'on_hold', 'dropped' or 'plan_to_watch' using all as default".format(collections[c][m]["status"])) - else: new_dictionary["status"] = util.mal_userlist_status[collections[c][m]["status"]] - if "sort_by" not in collections[c][m]: logger.warning("Collection Warning: mal_season sort_by attribute not found using score as default") - elif not collections[c][m]["sort_by"]: logger.warning("Collection Warning: mal_season sort_by attribute is blank using score as default") - elif collections[c][m]["sort_by"] not in util.mal_userlist_sort: logger.warning("Collection Warning: mal_season sort_by attribute {} invalid must be either 'score', 'last_updated', 'title' or 'start_date' using score as default".format(collections[c][m]["sort_by"])) - else: new_dictionary["sort_by"] = util.mal_userlist_sort[collections[c][m]["sort_by"]] - new_dictionary["limit"] = get_int(method_name, "limit", collections[c][m], 100, max=1000) - methods.append((method_name, [new_dictionary])) - else: - logger.error("Collection Error: {} attribute is not a dictionary: {}".format(m, collections[c][m])) - elif method_name in util.count_lists: - list_count = util.regex_first_int(collections[c][m], "List Size", default=20) - if list_count > 0: - methods.append((method_name, [list_count])) - else: - logger.error("Collection Error: {} must be an integer greater then 0 defaulting to 20".format(method_name)) - methods.append((method_name, [20])) - elif method_name in util.tmdb_lists: - values = self.TMDb.validate_tmdb_list(util.get_int_list(collections[c][m], "TMDb {} ID".format(util.tmdb_type[method_name])), util.tmdb_type[method_name]) - if method_name[-8:] == "_details": - if method_name in ["tmdb_collection_details", "tmdb_movie_details", "tmdb_show_details"]: - item = self.TMDb.get_movie_show_or_collection(values[0], library.is_movie) - if "summary" not in details and hasattr(item, "overview") and item.overview: - details["summary"] = item.overview - if "background" not in details and hasattr(item, "backdrop_path") and item.backdrop_path: - details["background"] = ("url", "{}{}".format(self.TMDb.image_url, item.backdrop_path), method_name[:-8]) - if "poster" not in details and hasattr(item, "poster_path") and item.poster_path: - details["poster"] = ("url", "{}{}".format(self.TMDb.image_url, item.poster_path), method_name[:-8]) - else: - item = self.TMDb.get_list(values[0]) - if "summary" not in details and hasattr(item, "description") and item.description: - details["summary"] = item.description - methods.append((method_name[:-8], values)) - else: - methods.append((method_name, values)) - elif method_name in util.all_lists: methods.append((method_name, util.get_list(collections[c][m]))) - elif method_name not in util.other_attributes: logger.error("Collection Error: {} attribute not supported".format(method_name)) - else: - logger.error("Collection Error: {} attribute is blank".format(m)) - except Failed as e: - logger.error(e) - - for i, f in enumerate(filters): + for i, f in enumerate(builder.filters): if i == 0: logger.info("") logger.info("Collection Filter {}: {}".format(f[0], f[1])) - do_arr = False - if library.Radarr: - do_arr = details["add_to_arr"] if "add_to_arr" in details else library.Radarr.add - if library.Sonarr: - do_arr = details["add_to_arr"] if "add_to_arr" in details else library.Sonarr.add - - movie_tag = details["arr_tag"] if "arr_tag" in details else None - show_tag = details["arr_tag"] if "arr_tag" in details else None - - items_found = 0 - - for method, values in methods: - logger.debug("") - logger.debug("Method: {}".format(method)) - logger.debug("Values: {}".format(values)) - pretty = util.pretty_names[method] if method in util.pretty_names else method - for value in values: - items = [] - missing_movies = [] - missing_shows = [] - def check_map(input_ids): - movie_ids, show_ids = input_ids - items_found_inside = 0 - if len(movie_ids) > 0: - items_found_inside += len(movie_ids) - for movie_id in movie_ids: - if movie_id in movie_map: items.append(movie_map[movie_id]) - else: missing_movies.append(movie_id) - if len(show_ids) > 0: - items_found_inside += len(show_ids) - for show_id in show_ids: - if show_id in show_map: items.append(show_map[show_id]) - else: missing_shows.append(show_id) - return items_found_inside - logger.info("") - logger.debug("Value: {}".format(value)) - if method == "plex_all": - logger.info("Processing {} {}".format(pretty, "Movies" if library.is_movie else "Shows")) - items = library.Plex.all() - items_found += len(items) - elif method == "plex_collection": - items = value.items() - items_found += len(items) - elif method == "plex_search": - search_terms = {} - output = "" - for i, attr_pair in enumerate(value): - search_list = attr_pair[1] - final_method = attr_pair[0][:-4] + "!" if attr_pair[0][-4:] == ".not" else attr_pair[0] - if library.is_show: - final_method = "show." + final_method - search_terms[final_method] = search_list - ors = "" - for o, param in enumerate(attr_pair[1]): - ors += "{}{}".format(" OR " if o > 0 else "{}(".format(attr_pair[0]), param) - logger.info("\t\t AND {})".format(ors) if i > 0 else "Processing {}: {})".format(pretty, ors)) - items = library.Plex.search(**search_terms) - items_found += len(items) - elif method == "plex_collectionless": - good_collections = [] - for col in library.get_all_collections(): - keep_collection = True - for pre in value["exclude_prefix"]: - if col.title.startswith(pre) or (col.titleSort and col.titleSort.startswith(pre)): - keep_collection = False - break - for ext in value["exclude"]: - if col.title == ext or (col.titleSort and col.titleSort == ext): - keep_collection = False - break - if keep_collection: - good_collections.append(col.title.lower()) - - all_items = library.Plex.all() - length = 0 - for i, item in enumerate(all_items, 1): - length = util.print_return(length, "Processing: {}/{} {}".format(i, len(all_items), item.title)) - add_item = True - for collection in item.collections: - if collection.tag.lower() in good_collections: - add_item = False - break - if add_item: - items.append(item) - items_found += len(items) - util.print_end(length, "Processed {} {}".format(len(all_items), "Movies" if library.is_movie else "Shows")) - elif "tautulli" in method: - items = library.Tautulli.get_items(library, time_range=value["list_days"], stats_count=value["list_size"], list_type=value["list_type"], stats_count_buffer=value["list_buffer"]) - items_found += len(items) - elif "anidb" in method: items_found += check_map(self.AniDB.get_items(method, value, library.Plex.language)) - elif "mal" in method: items_found += check_map(self.MyAnimeList.get_items(method, value)) - elif "tvdb" in method: items_found += check_map(self.TVDb.get_items(method, value, library.Plex.language)) - elif "imdb" in method: items_found += check_map(self.IMDb.get_items(method, value, library.Plex.language)) - elif "tmdb" in method: items_found += check_map(self.TMDb.get_items(method, value, library.is_movie)) - elif "trakt" in method: items_found += check_map(self.Trakt.get_items(method, value, library.is_movie)) - else: logger.error("Collection Error: {} method not supported".format(method)) - - if len(items) > 0: map = library.add_to_collection(collection_obj if collection_obj else collection_name, items, filters, show_filtered, map, movie_map, show_map) - else: logger.error("No items found to add to this collection ") - - if len(missing_movies) > 0 or len(missing_shows) > 0: - logger.info("") - if len(missing_movies) > 0: - not_lang = None - terms = None - for filter_method, filter_data in filters: - if filter_method.startswith("original_language"): - terms = util.get_list(filter_data, lower=True) - not_lang = filter_method.endswith(".not") - break - - missing_movies_with_names = [] - for missing_id in missing_movies: - try: - movie = self.TMDb.get_movie(missing_id) - title = str(movie.title) - if not_lang is None or (not_lang is True and movie.original_language not in terms) or (not_lang is False and movie.original_language in terms): - missing_movies_with_names.append((title, missing_id)) - logger.info("{} Collection | ? | {} (TMDb: {})".format(collection_name, title, missing_id)) - elif show_filtered is True: - logger.info("{} Collection | X | {} (TMDb: {})".format(collection_name, title, missing_id)) - except Failed as e: - logger.error(e) - logger.info("{} Movie{} Missing".format(len(missing_movies_with_names), "s" if len(missing_movies_with_names) > 1 else "")) - library.add_missing(collection_name, missing_movies_with_names, True) - if do_arr and library.Radarr: - library.Radarr.add_tmdb([missing_id for title, missing_id in missing_movies_with_names], tag=movie_tag) - if len(missing_shows) > 0 and library.is_show: - missing_shows_with_names = [] - for missing_id in missing_shows: - try: - title = str(self.TVDb.get_series(library.Plex.language, tvdb_id=missing_id).title.encode("ascii", "replace").decode()) - missing_shows_with_names.append((title, missing_id)) - logger.info("{} Collection | ? | {} (TVDB: {})".format(collection_name, title, missing_id)) - except Failed as e: - logger.error(e) - logger.info("{} Show{} Missing".format(len(missing_shows_with_names), "s" if len(missing_shows_with_names) > 1 else "")) - library.add_missing(c, missing_shows_with_names, False) - if do_arr and library.Sonarr: - library.Sonarr.add_tvdb([missing_id for title, missing_id in missing_shows_with_names], tag=show_tag) - - library.del_collection_if_empty(collection_name) - - if (sync_collection or collectionless) and items_found > 0: - logger.info("") - count_removed = 0 - for ratingKey, item in map.items(): - if item is not None: - logger.info("{} Collection | - | {}".format(collection_name, item.title)) - item.removeCollection(collection_name) - count_removed += 1 - logger.info("{} {}{} Removed".format(count_removed, "Movie" if library.is_movie else "Show", "s" if count_removed == 1 else "")) - logger.info("") + builder.run_methods(collection_obj, collection_name, map, movie_map, show_map) try: plex_collection = library.get_collection(collection_name) @@ -1038,82 +491,8 @@ class Config: logger.debug(e) continue - edits = {} - if "sort_title" in details: - edits["titleSort.value"] = details["sort_title"] - edits["titleSort.locked"] = 1 - if "content_rating" in details: - edits["contentRating.value"] = details["content_rating"] - edits["contentRating.locked"] = 1 - if "summary" in details: - edits["summary.value"] = details["summary"] - edits["summary.locked"] = 1 - if len(edits) > 0: - logger.debug(edits) - plex_collection.edit(**edits) - plex_collection.reload() - logger.info("Details: have been updated") - if "collection_mode" in details: - plex_collection.modeUpdate(mode=details["collection_mode"]) - if "collection_order" in details: - plex_collection.sortUpdate(sort=details["collection_order"]) - - if library.asset_directory: - name_mapping = c - if "name_mapping" in collections[c]: - if collections[c]["name_mapping"]: name_mapping = collections[c]["name_mapping"] - else: logger.error("Collection Error: name_mapping attribute is blank") - path = os.path.join(library.asset_directory, "{}".format(name_mapping), "poster.*") - matches = glob.glob(path) - if len(matches) > 0: - for match in matches: posters_found.append(("file", os.path.abspath(match), "asset_directory")) - elif len(posters_found) == 0 and "poster" not in details: logger.warning("poster not found at: {}".format(os.path.abspath(path))) - path = os.path.join(library.asset_directory, "{}".format(name_mapping), "background.*") - matches = glob.glob(path) - if len(matches) > 0: - for match in matches: backgrounds_found.append(("file", os.path.abspath(match), "asset_directory")) - elif len(backgrounds_found) == 0 and "background" not in details: logger.warning("background not found at: {}".format(os.path.abspath(path))) - - poster = util.choose_from_list(posters_found, "poster", list_type="tuple") - if not poster and "poster" in details: poster = details["poster"] - if poster: - if poster[0] == "url": plex_collection.uploadPoster(url=poster[1]) - else: plex_collection.uploadPoster(filepath=poster[1]) - logger.info("Detail: {} updated poster to [{}] {}".format(poster[2], poster[0], poster[1])) - - background = util.choose_from_list(backgrounds_found, "background", list_type="tuple") - if not background and "background" in details: background = details["background"] - if background: - if background[0] == "url": plex_collection.uploadArt(url=background[1]) - else: plex_collection.uploadArt(filepath=background[1]) - logger.info("Detail: {} updated background to [{}] {}".format(background[2], background[0], background[1])) - - if library.asset_directory: - path = os.path.join(library.asset_directory, "{}".format(name_mapping)) - if os.path.isdir(path): - dirs = [folder for folder in os.listdir(path) if os.path.isdir(os.path.join(path, folder))] - if len(dirs) > 0: - for item in plex_collection.items(): - folder = os.path.basename(os.path.dirname(item.locations[0])) - if folder in dirs: - files = [file for file in os.listdir(os.path.join(path, folder)) if os.path.isfile(os.path.join(path, folder, file))] - poster_path = None - background_path = None - for file in files: - if poster_path is None and file.startswith("poster."): - poster_path = os.path.join(path, folder, file) - if background_path is None and file.startswith("background."): - background_path = os.path.join(path, folder, file) - if poster_path: - item.uploadPoster(filepath=poster_path) - logger.info("Detail: asset_directory updated {}'s poster to [file] {}".format(item.title, poster_path)) - if background_path: - item.uploadArt(filepath=background_path) - logger.info("Detail: asset_directory updated {}'s background to [file] {}".format(item.title, background_path)) - if poster_path is None and background_path is None: - logger.warning("No Files Found: {}".format(os.path.join(path, folder))) - else: - logger.warning("No Folder: {}".format(os.path.join(path, folder))) + builder.update_details(plex_collection) + except Exception as e: util.print_stacktrace() logger.error("Unknown Error: {}".format(e)) diff --git a/modules/plex.py b/modules/plex.py index 710a5f67..6abf0926 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -29,17 +29,17 @@ class PlexAPI: 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("YAML Error: {}".format(str(e).replace("\n", "\n|\t "))) - self.metadata = None - if "metadata" in self.data: - if self.data["metadata"]: self.metadata = self.data["metadata"] - else: logger.warning("Config Warning: metadata attribute is blank") - else: logger.warning("Config Warning: metadata attribute not found") - - self.collections = None - if "collections" in self.data: - if self.data["collections"]: self.collections = self.data["collections"] - else: logger.warning("Config Warning: collections attribute is blank") - else: logger.warning("Config Warning: collections attribute not found") + 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.waring("Config Warning: {} must be a dictionary".format(attribute)) + else: logger.warning("Config Warning: {} attribute is blank".format(attribute)) + return None + + self.metadata = get_dict("metadata") + self.templates = get_dict("templates") + self.collections = get_dict("collections") if self.metadata is None and self.collections is None: raise Failed("YAML Error: metadata attributes or collections attribute required") @@ -116,20 +116,6 @@ class PlexAPI: raise Failed("Collection Error: No valid Plex Collections in {}".format(collections[c][m])) return valid_collections - def del_collection_if_empty(self, collection): - missing_data = {} - if not os.path.exists(self.missing_path): - with open(self.missing_path, "w"): pass - try: - missing_data, ind, bsi = yaml.util.load_yaml_guess_indent(open(self.missing_path)) - if not missing_data: - missing_data = {} - if collection in missing_data and len(missing_data[collection]) == 0: - del missing_data[collection] - yaml.round_trip_dump(missing_data, open(self.missing_path, "w"), indent=ind, block_seq_indent=bsi) - except yaml.scanner.ScannerError as e: - logger.error("YAML Error: {}".format(str(e).replace("\n", "\n|\t "))) - def add_missing(self, collection, items, is_movie): col_name = collection.encode("ascii", "replace").decode() if col_name not in self.missing: @@ -152,7 +138,8 @@ class PlexAPI: max_length = len(str(total)) length = 0 for i, item in enumerate(items, 1): - try: current = self.fetchItem(item.ratingKey if isinstance(item, (Movie, Show)) else int(item)) + try: + current = self.fetchItem(item.ratingKey if isinstance(item, (Movie, Show)) else int(item)) except (BadRequest, NotFound): logger.error("Plex Error: Item {} not found".format(item)) continue diff --git a/modules/trakt.py b/modules/trakt.py index c1f0f467..2bbcad27 100644 --- a/modules/trakt.py +++ b/modules/trakt.py @@ -89,7 +89,7 @@ class TraktAPI: lookup = Trakt["search"].lookup(external_id, from_source, media_type) if lookup: lookup = lookup[0] if isinstance(lookup, list) else lookup - if lookup.get_key(to_source)): + if lookup.get_key(to_source): return lookup.get_key(to_source) if to_source == "imdb" else int(lookup.get_key(to_source)) raise Failed("No {} ID found for {} ID {}".format(to_source.upper().replace("B", "b"), from_source.upper().replace("B", "b"), external_id)) diff --git a/modules/util.py b/modules/util.py index a2709cc7..e76212a9 100644 --- a/modules/util.py +++ b/modules/util.py @@ -254,6 +254,7 @@ collectionless_lists = [ other_attributes = [ "schedule", "sync_mode", + "template", "test", "tmdb_person" ]