import glob, json, logging, os, re, requests from datetime import datetime, timedelta from modules import util from modules.anidb import AniDBAPI from modules.cache import Cache from modules.imdb import IMDbAPI from modules.plex import PlexAPI from modules.mal import MyAnimeListAPI from modules.mal import MyAnimeListIDList from modules.tmdb import TMDbAPI from modules.trakt import TraktAPI from modules.tvdb import TVDbAPI from modules.util import Failed from plexapi.exceptions import BadRequest from ruamel import yaml logger = logging.getLogger("Plex Meta Manager") class Config: def __init__(self, default_dir, config_path=None): logger.info("Locating config...") if config_path and os.path.exists(config_path): self.config_path = os.path.abspath(config_path) elif config_path and not os.path.exists(config_path): raise Failed("Config Error: config not found at {}".format(os.path.abspath(config_path))) elif os.path.exists(os.path.join(default_dir, "config.yml")): self.config_path = os.path.abspath(os.path.join(default_dir, "config.yml")) else: raise Failed("Config Error: config not found at {}".format(os.path.abspath(default_dir))) logger.info("Using {} as config".format(self.config_path)) yaml.YAML().allow_duplicate_keys = True try: self.data, ind, bsi = yaml.util.load_yaml_guess_indent(open(self.config_path)) except yaml.scanner.ScannerError as e: raise Failed("YAML Error: {}".format(str(e).replace("\n", "\n|\t "))) def check_for_attribute(data, attribute, parent=None, test_list=None, options="", default=None, do_print=True, default_is_none=False, var_type="str", throw=False, save=True): message = "" endline = "" data = data if parent is None else data[parent] text = "{} attribute".format(attribute) if parent is None else "{} sub-attribute {}".format(parent, attribute) if data is None or attribute not in data: message = "Config Error: {} not found".format(text) if parent and save is True: new_config, ind, bsi = yaml.util.load_yaml_guess_indent(open(self.config_path)) endline = "\n| {} sub-attribute {} added to config".format(parent, attribute) if parent not in new_config: new_config = {parent: {attribute: default}} elif not new_config[parent]: new_config[parent] = {attribute: default} 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 var_type == "bool": if isinstance(data[attribute], bool): return data[attribute] else: message = "Config Error: {} must be either true or false".format(text) elif var_type == "int": if isinstance(data[attribute], int) and data[attribute] > 0: return data[attribute] else: message = "Config Error: {} must an integer > 0".format(text) elif var_type == "path": if os.path.exists(os.path.abspath(data[attribute])): return data[attribute] else: message = "Config Error: {} could not be found".format(text) if default and os.path.exists(os.path.abspath(default)): return default elif default: default = None default_is_none = True message = "Config Error: neither {} or the default path {} could be found".format(data[attribute], default) elif test_list is None or data[attribute] in test_list: return data[attribute] else: message = "Config Error: {}: {} is an invalid input".format(text, data[attribute]) if default is not None or default_is_none: message = message + " using {} as default".format(default) message = message + endline if (default is None and not default_is_none) or throw: if len(options) > 0: message = message + "\n" + options raise Failed(message) if do_print: util.print_multiline(message) if attribute in data and data[attribute] and test_list is not None and data[attribute] not in test_list: util.print_multiline(options) return default self.general = {} self.general["cache"] = check_for_attribute(self.data, "cache", parent="cache", options="| \ttrue (Create a cache to store ids)\n| \tfalse (Do not create a cache to store ids)", var_type="bool", default=True) if "cache" in self.data else True self.general["cache_expiration"] = check_for_attribute(self.data, "cache_expiration", parent="cache", var_type="int", default=60) if "cache" in self.data else 60 if self.general["cache"]: util.seperator() self.Cache = Cache(self.config_path, self.general["cache_expiration"]) else: self.Cache = None util.seperator() self.TMDb = None if "tmdb" in self.data: logger.info("Connecting to TMDb...") self.tmdb = {} self.tmdb["apikey"] = check_for_attribute(self.data, "apikey", parent="tmdb", throw=True) self.tmdb["language"] = check_for_attribute(self.data, "language", parent="tmdb", default="en") self.TMDb = TMDbAPI(self.tmdb) logger.info("TMDb Connection {}".format("Failed" if self.TMDb is None else "Successful")) else: raise Failed("Config Error: tmdb attribute not found") util.seperator() self.Trakt = None if "trakt" in self.data: logger.info("Connecting to Trakt...") self.trakt = {} try: self.trakt["client_id"] = check_for_attribute(self.data, "client_id", parent="trakt", throw=True) self.trakt["client_secret"] = check_for_attribute(self.data, "client_secret", parent="trakt", throw=True) self.trakt["config_path"] = self.config_path authorization = self.data["trakt"]["authorization"] if "authorization" in self.data["trakt"] and self.data["trakt"]["authorization"] else None self.Trakt = TraktAPI(self.trakt, authorization) except Failed as e: logger.error(e) logger.info("Trakt Connection {}".format("Failed" if self.Trakt is None else "Successful")) else: logger.warning("trakt attribute not found") util.seperator() self.MyAnimeList = None self.MyAnimeListIDList = MyAnimeListIDList() if "mal" in self.data: logger.info("Connecting to My Anime List...") self.mal = {} try: self.mal["client_id"] = check_for_attribute(self.data, "client_id", parent="mal", throw=True) self.mal["client_secret"] = check_for_attribute(self.data, "client_secret", parent="mal", throw=True) self.mal["config_path"] = self.config_path authorization = self.data["mal"]["authorization"] if "authorization" in self.data["mal"] and self.data["mal"]["authorization"] else None self.MyAnimeList = MyAnimeListAPI(self.mal, self.MyAnimeListIDList, authorization) except Failed as e: logger.error(e) logger.info("My Anime List Connection {}".format("Failed" if self.MyAnimeList is None else "Successful")) else: logger.warning("mal attribute not found") self.TVDb = TVDbAPI(Cache=self.Cache, TMDb=self.TMDb, Trakt=self.Trakt) 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) 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 self.general["plex"]["asset_directory"] = check_for_attribute(self.data, "asset_directory", parent="plex", var_type="path", default=os.path.join(default_dir, "assets")) if "plex" in self.data else os.path.join(default_dir, "assets") self.general["plex"]["sync_mode"] = check_for_attribute(self.data, "sync_mode", parent="plex", default="append", test_list=["append", "sync"], options="| \tappend (Only Add Items to the Collection)\n| \tsync (Add & Remove Items from the Collection)") if "plex" in self.data else "append" self.general["plex"]["show_unmanaged"] = check_for_attribute(self.data, "show_unmanaged", parent="plex", var_type="bool", default=True) if "plex" in self.data else True self.general["plex"]["show_filtered"] = check_for_attribute(self.data, "show_filtered", parent="plex", var_type="bool", default=False) if "plex" in self.data else False self.general["radarr"] = {} self.general["radarr"]["url"] = check_for_attribute(self.data, "url", parent="radarr", default_is_none=True) if "radarr" in self.data else None self.general["radarr"]["version"] = check_for_attribute(self.data, "version", parent="radarr", test_list=["v2", "v3"], options="| \tv2 (For Radarr 0.2)\n| \tv3 (For Radarr 3.0)", default="v2") if "radarr" in self.data else "v2" self.general["radarr"]["token"] = check_for_attribute(self.data, "token", parent="radarr", default_is_none=True) if "radarr" in self.data else None self.general["radarr"]["quality_profile"] = check_for_attribute(self.data, "quality_profile", parent="radarr", default_is_none=True) if "radarr" in self.data else None self.general["radarr"]["root_folder_path"] = check_for_attribute(self.data, "root_folder_path", parent="radarr", default_is_none=True) if "radarr" in self.data else None self.general["radarr"]["add"] = check_for_attribute(self.data, "add", parent="radarr", var_type="bool", default=False) if "radarr" in self.data else False self.general["radarr"]["search"] = check_for_attribute(self.data, "search", parent="radarr", var_type="bool", default=False) if "radarr" in self.data else False self.general["sonarr"] = {} self.general["sonarr"]["url"] = check_for_attribute(self.data, "url", parent="sonarr", default_is_none=True) if "sonarr" in self.data else None self.general["sonarr"]["token"] = check_for_attribute(self.data, "token", parent="sonarr", default_is_none=True) if "sonarr" in self.data else None self.general["sonarr"]["version"] = check_for_attribute(self.data, "version", parent="sonarr", test_list=["v2", "v3"], options="| \tv2 (For Sonarr 0.2)\n| \tv3 (For Sonarr 3.0)", default="v2") if "sonarr" in self.data else "v2" self.general["sonarr"]["quality_profile"] = check_for_attribute(self.data, "quality_profile", parent="sonarr", default_is_none=True) if "sonarr" in self.data else None self.general["sonarr"]["root_folder_path"] = check_for_attribute(self.data, "root_folder_path", parent="sonarr", default_is_none=True) if "sonarr" in self.data else None self.general["sonarr"]["add"] = check_for_attribute(self.data, "add", parent="sonarr", var_type="bool", default=False) if "sonarr" in self.data else False self.general["sonarr"]["search"] = check_for_attribute(self.data, "search", parent="sonarr", var_type="bool", default=False) if "sonarr" in self.data else False self.general["tautulli"] = {} 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) for lib in libs: util.seperator() params = {} if "library_name" in libs[lib] and libs[lib]["library_name"]: params["name"] = str(libs[lib]["library_name"]) logger.info("Connecting to {} ({}) Library...".format(params["name"], lib)) else: params["name"] = str(lib) logger.info("Connecting to {} Library...".format(params["name"])) default_lib = os.path.join(default_dir, "{}.yml".format(lib)) try: if "metadata_path" in libs[lib]: if libs[lib]["metadata_path"]: if os.path.exists(libs[lib]["metadata_path"]): params["metadata_path"] = libs[lib]["metadata_path"] else: raise Failed("metadata_path not found at {}".format(libs[lib]["metadata_path"])) else: raise Failed("metadata_path attribute is blank") else: if os.path.exists(default_lib): params["metadata_path"] = os.path.abspath(default_lib) else: raise Failed("default metadata_path not found at {}".format(os.path.abspath(os.path.join(default_dir, "{}.yml".format(params["name"]))))) if "library_type" in libs[lib]: if libs[lib]["library_type"]: if libs[lib]["library_type"] in ["movie", "show"]: params["library_type"] = libs[lib]["library_type"] else: raise Failed("library_type attribute must be either 'movie' or 'show'") else: raise Failed("library_type attribute is blank") else: raise Failed("library_type attribute is required") params["plex"] = {} if "plex" in libs[lib] and libs[lib]["plex"] and "url" in libs[lib]["plex"]: if libs[lib]["plex"]["url"]: params["plex"]["url"] = libs[lib]["plex"]["url"] else: raise Failed("url library attribute is blank") elif self.general["plex"]["url"]: params["plex"]["url"] = self.general["plex"]["url"] else: raise Failed("url attribute must be set under plex or under this specific Library") if "plex" in libs[lib] and libs[lib]["plex"] and "token" in libs[lib]["plex"]: if libs[lib]["plex"]["token"]: params["plex"]["token"] = libs[lib]["plex"]["token"] else: raise Failed("token library attribute is blank") elif self.general["plex"]["token"]: params["plex"]["token"] = self.general["plex"]["token"] else: raise Failed("token attribute must be set under plex or under this specific Library") except Failed as e: logger.error("Config Error: Skipping {} Library {}".format(str(lib), e)) continue params["asset_directory"] = None if "plex" in libs[lib] and "asset_directory" in libs[lib]["plex"]: if libs[lib]["plex"]["asset_directory"]: if os.path.exists(libs[lib]["plex"]["asset_directory"]): params["asset_directory"] = libs[lib]["plex"]["asset_directory"] else: logger.warning("Config Warning: Assets will not be used asset_directory not found at {}".format(libs[lib]["plex"]["asset_directory"])) else: logger.warning("Config Warning: Assets will not be used asset_directory library attribute is blank") elif self.general["plex"]["asset_directory"]: params["asset_directory"] = self.general["plex"]["asset_directory"] else: logger.warning("Config Warning: Assets will not be used asset_directory attribute must be set under config or under this specific Library") params["sync_mode"] = self.general["plex"]["sync_mode"] if "plex" in libs[lib] and "sync_mode" in libs[lib]["plex"]: if libs[lib]["plex"]["sync_mode"]: if libs[lib]["plex"]["sync_mode"] in ["append", "sync"]: params["sync_mode"] = libs[lib]["plex"]["sync_mode"] else: logger.warning("Config Warning: sync_mode attribute must be either 'append' or 'sync' using general value: {}".format(self.general["plex"]["sync_mode"])) else: logger.warning("Config Warning: sync_mode attribute is blank using general value: {}".format(self.general["plex"]["sync_mode"])) params["show_unmanaged"] = self.general["plex"]["show_unmanaged"] if "plex" in libs[lib] and "show_unmanaged" in libs[lib]["plex"]: if libs[lib]["plex"]["show_unmanaged"]: if isinstance(libs[lib]["plex"]["show_unmanaged"], bool): params["plex"]["show_unmanaged"] = libs[lib]["plex"]["show_unmanaged"] else: logger.warning("Config Warning: plex sub-attribute show_unmanaged must be either true or false using general value: {}".format(self.general["plex"]["show_unmanaged"])) else: logger.warning("Config Warning: plex sub-attribute show_unmanaged is blank using general value: {}".format(self.general["plex"]["show_unmanaged"])) params["show_filtered"] = self.general["plex"]["show_filtered"] if "plex" in libs[lib] and "show_filtered" in libs[lib]["plex"]: if libs[lib]["plex"]["show_filtered"]: if isinstance(libs[lib]["plex"]["show_filtered"], bool): params["plex"]["show_filtered"] = libs[lib]["plex"]["show_filtered"] else: logger.warning("Config Warning: plex sub-attribute show_filtered must be either true or false using general value: {}".format(self.general["plex"]["show_filtered"])) else: logger.warning("Config Warning: plex sub-attribute show_filtered is blank using general value: {}".format(self.general["plex"]["show_filtered"])) params["tmdb"] = self.TMDb params["tvdb"] = self.TVDb params["radarr"] = self.general["radarr"].copy() if "radarr" in libs[lib] and libs[lib]["radarr"]: if "url" in libs[lib]["radarr"]: if libs[lib]["radarr"]["url"]: params["radarr"]["url"] = libs[lib]["radarr"]["url"] else: logger.warning("Config Warning: radarr sub-attribute url is blank using general value: {}".format(self.general["radarr"]["url"])) if "token" in libs[lib]["radarr"]: if libs[lib]["radarr"]["token"]: params["radarr"]["token"] = libs[lib]["radarr"]["token"] else: logger.warning("Config Warning: radarr sub-attribute token is blank using general value: {}".format(self.general["radarr"]["token"])) if "version" in libs[lib]["radarr"]: if libs[lib]["radarr"]["version"]: if libs[lib]["radarr"]["version"] in ["v2", "v3"]: params["radarr"]["version"] = libs[lib]["radarr"]["version"] else: logger.warning("Config Warning: radarr sub-attribute version must be either 'v2' or 'v3' using general value: {}".format(self.general["radarr"]["version"])) else: logger.warning("Config Warning: radarr sub-attribute version is blank using general value: {}".format(self.general["radarr"]["version"])) if "quality_profile" in libs[lib]["radarr"]: if libs[lib]["radarr"]["quality_profile"]: params["radarr"]["quality_profile"] = libs[lib]["radarr"]["quality_profile"] else: logger.warning("Config Warning: radarr sub-attribute quality_profile is blank using general value: {}".format(self.general["radarr"]["quality_profile"])) if "root_folder_path" in libs[lib]["radarr"]: if libs[lib]["radarr"]["root_folder_path"]: params["radarr"]["root_folder_path"] = libs[lib]["radarr"]["root_folder_path"] else: logger.warning("Config Warning: radarr sub-attribute root_folder_path is blank using general value: {}".format(self.general["radarr"]["root_folder_path"])) if "add" in libs[lib]["radarr"]: if libs[lib]["radarr"]["add"]: if isinstance(libs[lib]["radarr"]["add"], bool): params["radarr"]["add"] = libs[lib]["radarr"]["add"] else: logger.warning("Config Warning: radarr sub-attribute add must be either true or false using general value: {}".format(self.general["radarr"]["add"])) else: logger.warning("Config Warning: radarr sub-attribute add is blank using general value: {}".format(self.general["radarr"]["add"])) if "search" in libs[lib]["radarr"]: if libs[lib]["radarr"]["search"]: if isinstance(libs[lib]["radarr"]["search"], bool): params["radarr"]["search"] = libs[lib]["radarr"]["search"] else: logger.warning("Config Warning: radarr sub-attribute search must be either true or false using general value: {}".format(self.general["radarr"]["search"])) else: logger.warning("Config Warning: radarr sub-attribute search is blank using general value: {}".format(self.general["radarr"]["search"])) if not params["radarr"]["url"] or not params["radarr"]["token"] or not params["radarr"]["quality_profile"] or not params["radarr"]["root_folder_path"]: params["radarr"] = None params["sonarr"] = self.general["sonarr"].copy() if "sonarr" in libs[lib] and libs[lib]["sonarr"]: if "url" in libs[lib]["sonarr"]: if libs[lib]["sonarr"]["url"]: params["sonarr"]["url"] = libs[lib]["sonarr"]["url"] else: logger.warning("Config Warning: sonarr sub-attribute url is blank using general value: {}".format(self.general["sonarr"]["url"])) if "token" in libs[lib]["sonarr"]: if libs[lib]["sonarr"]["token"]: params["sonarr"]["token"] = libs[lib]["sonarr"]["token"] else: logger.warning("Config Warning: sonarr sub-attribute token is blank using general value: {}".format(self.general["sonarr"]["token"])) if "version" in libs[lib]["sonarr"]: if libs[lib]["sonarr"]["version"]: if libs[lib]["sonarr"]["version"] in ["v2", "v3"]: params["sonarr"]["version"] = libs[lib]["sonarr"]["version"] else: logger.warning("Config Warning: sonarr sub-attribute version must be either 'v2' or 'v3' using general value: {}".format(self.general["sonarr"]["version"])) else: logger.warning("Config Warning: sonarr sub-attribute version is blank using general value: {}".format(self.general["sonarr"]["version"])) if "quality_profile" in libs[lib]["sonarr"]: if libs[lib]["sonarr"]["quality_profile"]: params["sonarr"]["quality_profile"] = libs[lib]["sonarr"]["quality_profile"] else: logger.warning("Config Warning: sonarr sub-attribute quality_profile is blank using general value: {}".format(self.general["sonarr"]["quality_profile"])) if "root_folder_path" in libs[lib]["sonarr"]: if libs[lib]["sonarr"]["root_folder_path"]: params["sonarr"]["root_folder_path"] = libs[lib]["sonarr"]["root_folder_path"] else: logger.warning("Config Warning: sonarr sub-attribute root_folder_path is blank using general value: {}".format(self.general["sonarr"]["root_folder_path"])) if "add" in libs[lib]["sonarr"]: if libs[lib]["sonarr"]["add"]: if isinstance(libs[lib]["sonarr"]["add"], bool): params["sonarr"]["add"] = libs[lib]["sonarr"]["add"] else: logger.warning("Config Warning: sonarr sub-attribute add must be either true or false using general value: {}".format(self.general["sonarr"]["add"])) else: logger.warning("Config Warning: sonarr sub-attribute add is blank using general value: {}".format(self.general["sonarr"]["add"])) if "search" in libs[lib]["sonarr"]: if libs[lib]["sonarr"]["search"]: if isinstance(libs[lib]["sonarr"]["search"], bool): params["sonarr"]["search"] = libs[lib]["sonarr"]["search"] else: logger.warning("Config Warning: sonarr sub-attribute search must be either true or false using general value: {}".format(self.general["sonarr"]["search"])) else: logger.warning("Config Warning: sonarr sub-attribute search is blank using general value: {}".format(self.general["sonarr"]["search"])) if not params["sonarr"]["url"] or not params["sonarr"]["token"] or not params["sonarr"]["quality_profile"] or not params["sonarr"]["root_folder_path"] or params["library_type"] == "movie": params["sonarr"] = None params["tautulli"] = self.general["tautulli"].copy() if "tautulli" in libs[lib] and libs[lib]["tautulli"]: if "url" in libs[lib]["tautulli"]: if libs[lib]["tautulli"]["url"]: params["tautulli"]["url"] = libs[lib]["tautulli"]["url"] else: logger.warning("Config Warning: tautulli sub-attribute url is blank using general value: {}".format(self.general["tautulli"]["url"])) if "apikey" in libs[lib]["tautulli"]: if libs[lib]["tautulli"]["apikey"]: params["tautulli"]["apikey"] = libs[lib]["tautulli"]["apikey"] else: logger.warning("Config Warning: tautulli sub-attribute apikey is blank using general value: {}".format(self.general["tautulli"]["apikey"])) if not params["tautulli"]["url"] or not params["tautulli"]["apikey"] : params["tautulli"] = None try: self.libraries.append(PlexAPI(params)) logger.info("{} Library Connection Successful".format(params["name"])) except Failed as e: logger.error(e) logger.info("{} Library Connection Failed".format(params["name"])) continue util.seperator() if len(self.libraries) > 0: logger.info("{} Plex Library Connection{} Successful".format(len(self.libraries), "s" if len(self.libraries) > 1 else "")) else: raise Failed("Plex Error: No Plex libraries were found") util.seperator() def update_libraries(self, test): for library in self.libraries: logger.info("") util.seperator("{} Library".format(library.name)) try: library.update_metadata(self.TMDb, test) except Failed as e: logger.error(e) logger.info("") util.seperator("{} Library {}Collections".format(library.name, "Test " if test else "")) collections = library.collections if collections: logger.info("") util.seperator("Mapping {} Library".format(library.name)) logger.info("") movie_map, show_map = self.map_guids(library) for c in collections: if test and ("test" not in collections[c] or collections[c]["test"] is not True): continue try: logger.info("") util.seperator("{} Collection".format(c)) 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)) continue try: collection_obj = library.get_collection(c) collection_name = collection_obj.title except Failed as e: 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: logger.info("Sync Mode: sync") if collection_obj: for item in collection_obj.items(): map[item.ratingKey] = item 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 == "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]): 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: No I") if "limit" in imdb_list and imdb_list["limit"]: list_count = util.regex_first_int(imdb_list["limit"], "List Limit", default=0) else: list_count = 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: if final_attr.startswith("tmdb_"): final_attr = final_attr[5:] 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): 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 items_found = 0 library.clear_collection_missing(collection_name) 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.save_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]) 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), "s" if len(missing_shows) > 1 else "")) library.save_missing(c, missing_shows_with_names, False) if do_arr and library.Sonarr: library.Sonarr.add_tvdb(missing_shows) 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("") try: plex_collection = library.get_collection(collection_name) except Failed as e: 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))) except Exception as e: util.print_stacktrace() logger.error("Unknown Error: {}".format(e)) if library.show_unmanaged is True and not test: logger.info("") util.seperator("Unmanaged Collections in {} Library".format(library.name)) logger.info("") unmanaged_count = 0 collections_in_plex = [str(pcol) for pcol in collections] for col in library.get_all_collections(): if col.title not in collections_in_plex: logger.info(col.title) unmanaged_count += 1 logger.info("{} Unmanaged Collections".format(unmanaged_count)) else: logger.info("") logger.error("No collection to update") def map_guids(self, library): movie_map = {} show_map = {} length = 0 count = 0 logger.info("Mapping {} Library: {}".format("Movie" if library.is_movie else "Show", library.name)) items = library.Plex.all() for i, item in enumerate(items, 1): length = util.print_return(length, "Processing: {}/{} {}".format(i, len(items), item.title)) try: id_type, main_id = self.get_id(item, library, length) except BadRequest: util.print_stacktrace() util.print_end(length, "{} {:<46} | {} for {}".format("Cache | ! |" if self.Cache else "Mapping Error:", item.guid, error_message, item.title)) continue if isinstance(main_id, list): if id_type == "movie": for m in main_id: movie_map[m] = item.ratingKey elif id_type == "show": for m in main_id: show_map[m] = item.ratingKey else: if id_type == "movie": movie_map[main_id] = item.ratingKey elif id_type == "show": show_map[main_id] = item.ratingKey util.print_end(length, "Processed {} {}".format(len(items), "Movies" if library.is_movie else "Shows")) return movie_map, show_map def get_id(self, item, library, length): expired = None tmdb_id = None imdb_id = None tvdb_id = None anidb_id = None mal_id = None error_message = None if self.Cache: if library.is_movie: tmdb_id, expired = self.Cache.get_tmdb_id("movie", plex_guid=item.guid) else: tvdb_id, expired = self.Cache.get_tvdb_id("show", plex_guid=item.guid) if not tvdb_id and library.is_show: tmdb_id, expired = self.Cache.get_tmdb_id("show", plex_guid=item.guid) anidb_id, expired = self.Cache.get_anidb_id("show", plex_guid=item.guid) if expired or (not tmdb_id and library.is_movie) or (not tvdb_id and not tmdb_id and library.is_show): guid = requests.utils.urlparse(item.guid) item_type = guid.scheme.split(".")[-1] check_id = guid.netloc if item_type == "plex" and library.is_movie: for guid_tag in item.guids: url_parsed = requests.utils.urlparse(guid_tag.id) if url_parsed.scheme == "tmdb": tmdb_id = url_parsed.netloc elif url_parsed.scheme == "imdb": imdb_id = url_parsed.netloc elif item_type == "imdb": imdb_id = check_id elif item_type == "thetvdb": tvdb_id = check_id elif item_type == "themoviedb": tmdb_id = check_id elif item_type == "hama": if check_id.startswith("tvdb"): tvdb_id = re.search("-(.*)", check_id).group(1) elif check_id.startswith("anidb"): anidb_id = re.search("-(.*)", check_id).group(1) else: error_message = "Hama Agent ID: {} not supported".format(check_id) elif item_type == "myanimelist": mal_id = check_id elif item_type == "local": error_message = "No match in Plex" else: error_message = "Agent {} not supported".format(item_type) if not error_message: if anidb_id and not tvdb_id: try: tvdb_id = self.AniDB.convert_anidb_to_tvdb(anidb_id) except Failed: pass if anidb_id and not imdb_id: try: imdb_id = self.AniDB.convert_anidb_to_imdb(anidb_id) except Failed: pass if mal_id: try: ids = self.MyAnimeListIDList.find_mal_ids(mal_id) if "thetvdb_id" in ids and int(ids["thetvdb_id"]) > 0: tvdb_id = int(ids["thetvdb_id"]) elif "themoviedb_id" in ids and int(ids["themoviedb_id"]) > 0: tmdb_id = int(ids["themoviedb_id"]) else: raise Failed("MyAnimeList Error: MyAnimeList ID: {} has no other IDs associated with it".format(mal_id)) except Failed: pass if mal_id and not tvdb_id: try: tvdb_id = self.MyAnimeListIDList.convert_mal_to_tvdb(mal_id) except Failed: pass if mal_id and not tmdb_id: try: tmdb_id = self.MyAnimeListIDList.convert_mal_to_tmdb(mal_id) except Failed: pass if not tmdb_id and imdb_id and isinstance(imdb_id, list) and self.TMDb: tmdb_id = [] new_imdb_id = [] for imdb in imdb_id: try: temp_tmdb_id = self.TMDb.convert_imdb_to_tmdb(imdb) tmdb_id.append(temp_tmdb_id) new_imdb_id.append(imdb) except Failed: continue imdb_id = new_imdb_id if not tmdb_id and imdb_id and self.TMDb: try: tmdb_id = self.TMDb.convert_imdb_to_tmdb(imdb_id) except Failed: pass if not tmdb_id and imdb_id and self.Trakt: try: tmdb_id = self.Trakt.convert_imdb_to_tmdb(imdb_id) except Failed: pass if not tmdb_id and tvdb_id and self.TMDb: try: tmdb_id = self.TMDb.convert_tvdb_to_tmdb(tvdb_id) except Failed: pass if not tmdb_id and tvdb_id and self.Trakt: try: tmdb_id = self.Trakt.convert_tvdb_to_tmdb(tvdb_id) except Failed: pass if not imdb_id and tmdb_id and self.TMDb: try: imdb_id = self.TMDb.convert_tmdb_to_imdb(tmdb_id) except Failed: pass if not imdb_id and tmdb_id and self.Trakt: try: imdb_id = self.Trakt.convert_tmdb_to_imdb(tmdb_id) except Failed: pass if not imdb_id and tvdb_id and self.Trakt: try: imdb_id = self.Trakt.convert_tmdb_to_imdb(tmdb_id) except Failed: pass if not tvdb_id and tmdb_id and self.TMDb and library.is_show: try: tvdb_id = self.TMDb.convert_tmdb_to_tvdb(tmdb_id) except Failed: pass if not tvdb_id and tmdb_id and self.Trakt and library.is_show: try: tvdb_id = self.Trakt.convert_tmdb_to_tvdb(tmdb_id) except Failed: pass if not tvdb_id and imdb_id and self.Trakt and library.is_show: try: tvdb_id = self.Trakt.convert_imdb_to_tvdb(imdb_id) except Failed: pass if (not tmdb_id and library.is_movie) or (not tvdb_id and not ((anidb_id or mal_id) and tmdb_id) and library.is_show): service_name = "TMDb ID" if library.is_movie else "TVDb ID" if self.TMDb and self.Trakt: api_name = "TMDb or Trakt" elif self.TMDb: api_name = "TMDb" elif self.Trakt: api_name = "Trakt" else: api_name = None if tmdb_id and imdb_id: id_name = "TMDb ID: {} or IMDb ID: {}".format(tmdb_id, imdb_id) elif imdb_id and tvdb_id: id_name = "IMDb ID: {} or TVDb ID: {}".format(imdb_id, tvdb_id) elif tmdb_id: id_name = "TMDb ID: {}".format(tmdb_id) elif imdb_id: id_name = "IMDb ID: {}".format(imdb_id) elif tvdb_id: id_name = "TVDb ID: {}".format(tvdb_id) else: id_name = None if anidb_id and not tmdb_id and not tvdb_id: error_message = "Unable to convert AniDb ID: {} to TMDb ID or TVDb ID".format(anidb_id) elif mal_id and not tmdb_id and not tvdb_id: error_message = "Unable to convert MyAnimeList ID: {} to TMDb ID or TVDb ID".format(mal_id) elif id_name and api_name: error_message = "Unable to convert {} to {} using {}".format(id_name, service_name, api_name) elif id_name: error_message = "Configure TMDb or Trakt to covert {} to {}".format(id_name, service_name) else: error_message = "No ID to convert to {}".format(service_name) if self.Cache and (tmdb_id and library.is_movie) or ((tvdb_id or ((anidb_id or mal_id) and tmdb_id)) and library.is_show): if isinstance(tmdb_id, list): for i in range(len(tmdb_id)): util.print_end(length, "Cache | {} | {:<46} | {:<6} | {:<10} | {:<6} | {:<5} | {:<5} | {}".format("^" if expired is True else "+", item.guid, tmdb_id[i] if tmdb_id[i] else "None", imdb_id[i] if imdb_id[i] else "None", tvdb_id if tvdb_id else "None", anidb_id if anidb_id else "None", mal_id if mal_id else "None", item.title)) self.Cache.update_guid("movie" if library.is_movie else "show", item.guid, tmdb_id[i], imdb_id[i], tvdb_id, anidb_id, mal_id, expired) else: util.print_end(length, "Cache | {} | {:<46} | {:<6} | {:<10} | {:<6} | {:<5} | {:<5} | {}".format("^" if expired is True else "+", item.guid, tmdb_id if tmdb_id else "None", imdb_id if imdb_id else "None", tvdb_id if tvdb_id else "None", anidb_id if anidb_id else "None", mal_id if mal_id else "None", item.title)) self.Cache.update_guid("movie" if library.is_movie else "show", item.guid, tmdb_id, imdb_id, tvdb_id, anidb_id, mal_id, expired) if tmdb_id and library.is_movie: return "movie", tmdb_id elif tvdb_id and library.is_show: return "show", tvdb_id elif (anidb_id or mal_id) and tmdb_id: return "movie", tmdb_id else: util.print_end(length, "{} {:<46} | {} for {}".format("Cache | ! |" if self.Cache else "Mapping Error:", item.guid, error_message, item.title)) return None, None