import logging, os, re, requests, time from modules import util from modules.anidb import AniDBAPI from modules.anilist import AniListAPI from modules.arms import ArmsAPI from modules.builder import CollectionBuilder from modules.cache import Cache from modules.imdb import IMDbAPI from modules.letterboxd import LetterboxdAPI from modules.mal import MyAnimeListAPI from modules.omdb import OMDbAPI from modules.plex import PlexAPI from modules.radarr import RadarrAPI from modules.sonarr import SonarrAPI from modules.tautulli import TautulliAPI from modules.tmdb import TMDbAPI from modules.trakttv import TraktAPI from modules.tvdb import TVDbAPI from modules.util import Failed from plexapi.exceptions import BadRequest from retrying import retry from ruamel import yaml logger = logging.getLogger("Plex Meta Manager") sync_modes = {"append": "Only Add Items to the Collection", "sync": "Add & Remove Items from the Collection"} radarr_versions = {"v2": "For Radarr 0.2", "v3": "For Radarr 3.0"} radarr_availabilities = { "announced": "For Announced", "cinemas": "For In Cinemas", "released": "For Released", "db": "For PreDB" } sonarr_versions = {"v2": "For Sonarr 0.2", "v3": "For Sonarr 3.0"} sonarr_monitors = { "all": "Monitor all episodes except specials", "future": "Monitor episodes that have not aired yet", "missing": "Monitor episodes that do not have files or have not aired yet", "existing": "Monitor episodes that have files or have not aired yet", "pilot": "Monitor the first episode. All other episodes will be ignored", "first": "Monitor all episodes of the first season. All other seasons will be ignored", "latest": "Monitor all episodes of the latest season and future seasons", "none": "No episodes will be monitored" } sonarr_series_types = { "standard": "Episodes released with SxxEyy pattern", "daily": "Episodes released daily or less frequently that use year-month-day (2017-05-25)", "anime": "Episodes released using an absolute episode number" } mass_update_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb"} library_types = {"movie": "For Movie Libraries", "show": "For Show Libraries"} class Config: def __init__(self, default_dir, config_path=None, libraries_to_run=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(f"Config Error: config not found at {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(f"Config Error: config not found at {os.path.abspath(default_dir)}") logger.info(f"Using {self.config_path} as config") yaml.YAML().allow_duplicate_keys = True try: new_config, ind, bsi = yaml.util.load_yaml_guess_indent(open(self.config_path, encoding="utf-8")) def replace_attr(all_data, attr, par): if "settings" not in all_data: all_data["settings"] = {} if par in all_data and all_data[par] and attr in all_data[par] and attr not in all_data["settings"]: all_data["settings"][attr] = all_data[par][attr] del all_data[par][attr] if "libraries" not in new_config: new_config["libraries"] = {} if "settings" not in new_config: new_config["settings"] = {} if "tmdb" not in new_config: new_config["tmdb"] = {} replace_attr(new_config, "cache", "cache") replace_attr(new_config, "cache_expiration", "cache") if "config" in new_config: del new_config["cache"] replace_attr(new_config, "asset_directory", "plex") replace_attr(new_config, "sync_mode", "plex") replace_attr(new_config, "show_unmanaged", "plex") replace_attr(new_config, "show_filtered", "plex") replace_attr(new_config, "show_missing", "plex") replace_attr(new_config, "save_missing", "plex") if new_config["libraries"]: for library in new_config["libraries"]: if new_config["libraries"][library] and "plex" in new_config["libraries"][library]: replace_attr(new_config["libraries"][library], "asset_directory", "plex") replace_attr(new_config["libraries"][library], "sync_mode", "plex") replace_attr(new_config["libraries"][library], "show_unmanaged", "plex") replace_attr(new_config["libraries"][library], "show_filtered", "plex") replace_attr(new_config["libraries"][library], "show_missing", "plex") replace_attr(new_config["libraries"][library], "save_missing", "plex") if "libraries" in new_config: new_config["libraries"] = new_config.pop("libraries") if "settings" in new_config: new_config["settings"] = new_config.pop("settings") if "plex" in new_config: new_config["plex"] = new_config.pop("plex") if "tmdb" in new_config: new_config["tmdb"] = new_config.pop("tmdb") if "tautulli" in new_config: new_config["tautulli"] = new_config.pop("tautulli") if "radarr" in new_config: new_config["radarr"] = new_config.pop("radarr") if "sonarr" in new_config: new_config["sonarr"] = new_config.pop("sonarr") if "omdb" in new_config: new_config["omdb"] = new_config.pop("omdb") if "trakt" in new_config: new_config["trakt"] = new_config.pop("trakt") if "mal" in new_config: new_config["mal"] = new_config.pop("mal") yaml.round_trip_dump(new_config, open(self.config_path, "w", encoding="utf-8"), indent=ind, block_seq_indent=bsi) self.data = new_config except yaml.scanner.ScannerError as e: raise Failed(f"YAML Error: {util.tab_new_lines(e)}") except Exception as e: util.print_stacktrace() raise Failed(f"YAML Error: {e}") def check_for_attribute(data, attribute, parent=None, test_list=None, default=None, do_print=True, default_is_none=False, req_default=False, var_type="str", throw=False, save=True): endline = "" if parent is not None: if data and parent in data: data = data[parent] else: data = None do_print = False save = False text = f"{attribute} attribute" if parent is None else f"{parent} sub-attribute {attribute}" if data is None or attribute not in data: message = f"{text} not found" if parent and save is True: loaded_config, ind_in, bsi_in = yaml.util.load_yaml_guess_indent(open(self.config_path)) endline = f"\n{parent} sub-attribute {attribute} added to config" if parent not in loaded_config or not loaded_config[parent]: loaded_config[parent] = {attribute: default} elif attribute not in loaded_config[parent]: loaded_config[parent][attribute] = default else: endline = "" yaml.round_trip_dump(loaded_config, open(self.config_path, "w"), indent=ind_in, block_seq_indent=bsi_in) elif data[attribute] is None: if default_is_none is True: return None else: message = f"{text} is blank" elif var_type == "bool": if isinstance(data[attribute], bool): return data[attribute] else: message = f"{text} must be either true or false" elif var_type == "int": if isinstance(data[attribute], int) and data[attribute] >= 0: return data[attribute] else: message = f"{text} must an integer >= 0" elif var_type == "path": if os.path.exists(os.path.abspath(data[attribute])): return data[attribute] else: message = f"Path {os.path.abspath(data[attribute])} does not exist" elif var_type == "list": return util.get_list(data[attribute]) elif var_type == "list_path": temp_list = [p for p in util.get_list(data[attribute], split=True) if os.path.exists(os.path.abspath(p))] if len(temp_list) > 0: return temp_list else: message = "No Paths exist" elif var_type == "lower_list": return util.get_list(data[attribute], lower=True) elif test_list is None or data[attribute] in test_list: return data[attribute] else: message = f"{text}: {data[attribute]} is an invalid input" if var_type == "path" and default and os.path.exists(os.path.abspath(default)): return default elif var_type == "path" and default: if data and attribute in data and data[attribute]: message = f"neither {data[attribute]} or the default path {default} could be found" else: message = f"no {text} found and the default path {default} could not be found" default = None if default is not None or default_is_none: message = message + f" using {default} as default" message = message + endline if req_default and default is None: raise Failed(f"Config Error: {attribute} attribute must be set under {parent} globally or under this specific Library") options = "" if test_list: for option, description in test_list.items(): if len(options) > 0: options = f"{options}\n" options = f"{options} {option} ({description})" if (default is None and not default_is_none) or throw: if len(options) > 0: message = message + "\n" + options raise Failed(f"Config Error: {message}") if do_print: util.print_multiline(f"Config Warning: {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="settings", var_type="bool", default=True) self.general["cache_expiration"] = check_for_attribute(self.data, "cache_expiration", parent="settings", var_type="int", default=60) if self.general["cache"]: util.separator() self.Cache = Cache(self.config_path, self.general["cache_expiration"]) else: self.Cache = None self.general["asset_directory"] = check_for_attribute(self.data, "asset_directory", parent="settings", var_type="list_path", default=[os.path.join(default_dir, "assets")]) self.general["asset_folders"] = check_for_attribute(self.data, "asset_folders", parent="settings", var_type="bool", default=True) self.general["assets_for_all"] = check_for_attribute(self.data, "assets_for_all", parent="settings", var_type="bool", default=False) self.general["sync_mode"] = check_for_attribute(self.data, "sync_mode", parent="settings", default="append", test_list=sync_modes) self.general["run_again_delay"] = check_for_attribute(self.data, "run_again_delay", parent="settings", var_type="int", default=0) self.general["show_unmanaged"] = check_for_attribute(self.data, "show_unmanaged", parent="settings", var_type="bool", default=True) self.general["show_filtered"] = check_for_attribute(self.data, "show_filtered", parent="settings", var_type="bool", default=False) self.general["show_missing"] = check_for_attribute(self.data, "show_missing", parent="settings", var_type="bool", default=True) self.general["save_missing"] = check_for_attribute(self.data, "save_missing", parent="settings", var_type="bool", default=True) util.separator() self.TMDb = None if "tmdb" in self.data: logger.info("Connecting to TMDb...") self.tmdb = {} try: self.tmdb["apikey"] = check_for_attribute(self.data, "apikey", parent="tmdb", throw=True) except Failed as e: raise Failed(e) self.tmdb["language"] = check_for_attribute(self.data, "language", parent="tmdb", default="en") self.TMDb = TMDbAPI(self, self.tmdb) logger.info(f"TMDb Connection {'Failed' if self.TMDb is None else 'Successful'}") else: raise Failed("Config Error: tmdb attribute not found") util.separator() self.OMDb = None if "omdb" in self.data: logger.info("Connecting to OMDb...") self.omdb = {} try: self.omdb["apikey"] = check_for_attribute(self.data, "apikey", parent="omdb", throw=True) self.OMDb = OMDbAPI(self.omdb, Cache=self.Cache) except Failed as e: logger.error(e) logger.info(f"OMDb Connection {'Failed' if self.OMDb is None else 'Successful'}") else: logger.warning("omdb attribute not found") util.separator() 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(f"Trakt Connection {'Failed' if self.Trakt is None else 'Successful'}") else: logger.warning("trakt attribute not found") util.separator() self.MyAnimeList = None 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, authorization) except Failed as e: logger.error(e) logger.info(f"My Anime List Connection {'Failed' if self.MyAnimeList is None else 'Successful'}") else: logger.warning("mal attribute not found") self.TVDb = TVDbAPI(self) self.IMDb = IMDbAPI(self) self.AniDB = AniDBAPI(self) self.Arms = ArmsAPI(self) self.AniList = AniListAPI(self) self.Letterboxd = LetterboxdAPI(self) util.separator() 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) self.general["plex"]["token"] = check_for_attribute(self.data, "token", parent="plex", default_is_none=True) self.general["plex"]["timeout"] = check_for_attribute(self.data, "timeout", parent="plex", var_type="int", default=60) self.general["radarr"] = {} self.general["radarr"]["url"] = check_for_attribute(self.data, "url", parent="radarr", default_is_none=True) self.general["radarr"]["token"] = check_for_attribute(self.data, "token", parent="radarr", default_is_none=True) self.general["radarr"]["version"] = check_for_attribute(self.data, "version", parent="radarr", test_list=radarr_versions, default="v3") self.general["radarr"]["add"] = check_for_attribute(self.data, "add", parent="radarr", var_type="bool", default=False) self.general["radarr"]["root_folder_path"] = check_for_attribute(self.data, "root_folder_path", parent="radarr", default_is_none=True) self.general["radarr"]["monitor"] = check_for_attribute(self.data, "monitor", parent="radarr", var_type="bool", default=True) self.general["radarr"]["availability"] = check_for_attribute(self.data, "availability", parent="radarr", test_list=radarr_availabilities, default="announced") self.general["radarr"]["quality_profile"] = check_for_attribute(self.data, "quality_profile", parent="radarr", default_is_none=True) self.general["radarr"]["tag"] = check_for_attribute(self.data, "tag", parent="radarr", var_type="lower_list", default_is_none=True) self.general["radarr"]["search"] = check_for_attribute(self.data, "search", parent="radarr", var_type="bool", default=False) self.general["sonarr"] = {} self.general["sonarr"]["url"] = check_for_attribute(self.data, "url", parent="sonarr", default_is_none=True) self.general["sonarr"]["token"] = check_for_attribute(self.data, "token", parent="sonarr", default_is_none=True) self.general["sonarr"]["version"] = check_for_attribute(self.data, "version", parent="sonarr", test_list=sonarr_versions, default="v3") self.general["sonarr"]["add"] = check_for_attribute(self.data, "add", parent="sonarr", var_type="bool", default=False) self.general["sonarr"]["root_folder_path"] = check_for_attribute(self.data, "root_folder_path", parent="sonarr", default_is_none=True) self.general["sonarr"]["monitor"] = check_for_attribute(self.data, "monitor", parent="sonarr", test_list=sonarr_monitors, default="all") self.general["sonarr"]["quality_profile"] = check_for_attribute(self.data, "quality_profile", parent="sonarr", default_is_none=True) self.general["sonarr"]["language_profile"] = check_for_attribute(self.data, "language_profile", parent="sonarr", default_is_none=True) self.general["sonarr"]["series_type"] = check_for_attribute(self.data, "series_type", parent="sonarr", test_list=sonarr_series_types, default="standard") self.general["sonarr"]["season_folder"] = check_for_attribute(self.data, "season_folder", parent="sonarr", var_type="bool", default=True) self.general["sonarr"]["tag"] = check_for_attribute(self.data, "tag", parent="sonarr", var_type="lower_list", default_is_none=True) self.general["sonarr"]["search"] = check_for_attribute(self.data, "search", parent="sonarr", var_type="bool", default=False) self.general["sonarr"]["cutoff_search"] = check_for_attribute(self.data, "cutoff_search", parent="sonarr", var_type="bool", default=False) self.general["tautulli"] = {} self.general["tautulli"]["url"] = check_for_attribute(self.data, "url", parent="tautulli", default_is_none=True) self.general["tautulli"]["apikey"] = check_for_attribute(self.data, "apikey", parent="tautulli", default_is_none=True) self.libraries = [] try: libs = check_for_attribute(self.data, "libraries", throw=True) except Failed as e: raise Failed(e) requested_libraries = util.get_list(libraries_to_run) if libraries_to_run else None for library_name, lib in libs.items(): if requested_libraries and library_name not in requested_libraries: continue util.separator() params = {} logger.info("") if lib and "library_name" in lib and lib["library_name"]: params["name"] = str(lib["library_name"]) logger.info(f"Connecting to {params['name']} ({library_name}) Library...") else: params["name"] = str(library_name) logger.info(f"Connecting to {params['name']} Library...") params["asset_directory"] = check_for_attribute(lib, "asset_directory", parent="settings", var_type="list_path", default=self.general["asset_directory"], default_is_none=True, save=False) if params["asset_directory"] is None: logger.warning("Config Warning: Assets will not be used asset_directory attribute must be set under config or under this specific Library") if lib and "settings" in lib and lib["settings"] and "asset_folders" in lib["settings"]: params["asset_folders"] = check_for_attribute(lib, "asset_folders", parent="settings", var_type="bool", default=self.general["asset_folders"], do_print=False, save=False) else: params["asset_folders"] = check_for_attribute(lib, "asset_folders", var_type="bool", default=self.general["asset_folders"], do_print=False, save=False) if lib and "settings" in lib and lib["settings"] and "assets_for_all" in lib["settings"]: params["assets_for_all"] = check_for_attribute(lib, "assets_for_all", parent="settings", var_type="bool", default=self.general["assets_for_all"], do_print=False, save=False) else: params["assets_for_all"] = check_for_attribute(lib, "assets_for_all", var_type="bool", default=self.general["assets_for_all"], do_print=False, save=False) if lib and "settings" in lib and lib["settings"] and "sync_mode" in lib["settings"]: params["sync_mode"] = check_for_attribute(lib, "sync_mode", parent="settings", test_list=sync_modes, default=self.general["sync_mode"], do_print=False, save=False) else: params["sync_mode"] = check_for_attribute(lib, "sync_mode", test_list=sync_modes, default=self.general["sync_mode"], do_print=False, save=False) if lib and "settings" in lib and lib["settings"] and "show_unmanaged" in lib["settings"]: params["show_unmanaged"] = check_for_attribute(lib, "show_unmanaged", parent="settings", var_type="bool", default=self.general["show_unmanaged"], do_print=False, save=False) else: params["show_unmanaged"] = check_for_attribute(lib, "show_unmanaged", var_type="bool", default=self.general["show_unmanaged"], do_print=False, save=False) if lib and "settings" in lib and lib["settings"] and "show_filtered" in lib["settings"]: params["show_filtered"] = check_for_attribute(lib, "show_filtered", parent="settings", var_type="bool", default=self.general["show_filtered"], do_print=False, save=False) else: params["show_filtered"] = check_for_attribute(lib, "show_filtered", var_type="bool", default=self.general["show_filtered"], do_print=False, save=False) if lib and "settings" in lib and lib["settings"] and "show_missing" in lib["settings"]: params["show_missing"] = check_for_attribute(lib, "show_missing", parent="settings", var_type="bool", default=self.general["show_missing"], do_print=False, save=False) else: params["show_missing"] = check_for_attribute(lib, "show_missing", var_type="bool", default=self.general["show_missing"], do_print=False, save=False) if lib and "settings" in lib and lib["settings"] and "save_missing" in lib["settings"]: params["save_missing"] = check_for_attribute(lib, "save_missing", parent="settings", var_type="bool", default=self.general["save_missing"], do_print=False, save=False) else: params["save_missing"] = check_for_attribute(lib, "save_missing", var_type="bool", default=self.general["save_missing"], do_print=False, save=False) if lib and "mass_genre_update" in lib and lib["mass_genre_update"]: params["mass_genre_update"] = check_for_attribute(lib, "mass_genre_update", test_list=mass_update_options, default_is_none=True, save=False) if self.OMDb is None and params["mass_genre_update"] == "omdb": params["mass_genre_update"] = None logger.error("Config Error: mass_genre_update cannot be omdb without a successful OMDb Connection") else: params["mass_genre_update"] = None if lib and "mass_audience_rating_update" in lib and lib["mass_audience_rating_update"]: params["mass_audience_rating_update"] = check_for_attribute(lib, "mass_audience_rating_update", test_list=mass_update_options, default_is_none=True, save=False) if self.OMDb is None and params["mass_audience_rating_update"] == "omdb": params["mass_audience_rating_update"] = None logger.error("Config Error: mass_audience_rating_update cannot be omdb without a successful OMDb Connection") else: params["mass_audience_rating_update"] = None try: if lib and "metadata_path" in lib: params["metadata_path"] = [] if lib["metadata_path"] is None: raise Failed("Config Error: metadata_path attribute is blank") paths_to_check = lib["metadata_path"] if isinstance(lib["metadata_path"], list) else [lib["metadata_path"]] for path in paths_to_check: if isinstance(path, dict): if "url" in path: if path["url"] is None: logger.error("Config Error: metadata_path url is blank") else: params["metadata_path"].append(("URL", path["url"])) if "git" in path: if path["git"] is None: logger.error("Config Error: metadata_path git is blank") else: params["metadata_path"].append(("Git", path['git'])) else: params["metadata_path"].append(("File", path)) else: params["metadata_path"] = [("File", os.path.join(default_dir, f"{library_name}.yml"))] params["default_dir"] = default_dir params["plex"] = {} params["plex"]["url"] = check_for_attribute(lib, "url", parent="plex", default=self.general["plex"]["url"], req_default=True, save=False) params["plex"]["token"] = check_for_attribute(lib, "token", parent="plex", default=self.general["plex"]["token"], req_default=True, save=False) params["plex"]["timeout"] = check_for_attribute(lib, "timeout", parent="plex", var_type="int", default=self.general["plex"]["timeout"], save=False) library = PlexAPI(params, self.TMDb, self.TVDb) logger.info(f"{params['name']} Library Connection Successful") except Failed as e: util.print_multiline(e, error=True) logger.info(f"{params['name']} Library Connection Failed") continue if self.general["radarr"]["url"] or (lib and "radarr" in lib): logger.info("") logger.info(f"Connecting to {params['name']} library's Radarr...") radarr_params = {} try: radarr_params["url"] = check_for_attribute(lib, "url", parent="radarr", default=self.general["radarr"]["url"], req_default=True, save=False) radarr_params["token"] = check_for_attribute(lib, "token", parent="radarr", default=self.general["radarr"]["token"], req_default=True, save=False) radarr_params["version"] = check_for_attribute(lib, "version", parent="radarr", test_list=radarr_versions, default=self.general["radarr"]["version"], save=False) radarr_params["add"] = check_for_attribute(lib, "add", parent="radarr", var_type="bool", default=self.general["radarr"]["add"], save=False) radarr_params["root_folder_path"] = check_for_attribute(lib, "root_folder_path", parent="radarr", default=self.general["radarr"]["root_folder_path"], req_default=True, save=False) radarr_params["monitor"] = check_for_attribute(lib, "monitor", parent="radarr", var_type="bool", default=self.general["radarr"]["monitor"], save=False) radarr_params["availability"] = check_for_attribute(lib, "availability", parent="radarr", test_list=radarr_availabilities, default=self.general["radarr"]["availability"], save=False) radarr_params["quality_profile"] = check_for_attribute(lib, "quality_profile", parent="radarr", default=self.general["radarr"]["quality_profile"], req_default=True, save=False) radarr_params["tag"] = check_for_attribute(lib, "search", parent="radarr", var_type="lower_list", default=self.general["radarr"]["tag"], default_is_none=True, save=False) radarr_params["search"] = check_for_attribute(lib, "search", parent="radarr", var_type="bool", default=self.general["radarr"]["search"], save=False) library.Radarr = RadarrAPI(radarr_params) except Failed as e: util.print_multiline(e, error=True) logger.info(f"{params['name']} library's Radarr Connection {'Failed' if library.Radarr is None else 'Successful'}") if self.general["sonarr"]["url"] or (lib and "sonarr" in lib): logger.info("") logger.info(f"Connecting to {params['name']} library's Sonarr...") sonarr_params = {} try: sonarr_params["url"] = check_for_attribute(lib, "url", parent="sonarr", default=self.general["sonarr"]["url"], req_default=True, save=False) sonarr_params["token"] = check_for_attribute(lib, "token", parent="sonarr", default=self.general["sonarr"]["token"], req_default=True, save=False) sonarr_params["version"] = check_for_attribute(lib, "version", parent="sonarr", test_list=sonarr_versions, default=self.general["sonarr"]["version"], save=False) sonarr_params["add"] = check_for_attribute(lib, "add", parent="sonarr", var_type="bool", default=self.general["sonarr"]["add"], save=False) sonarr_params["root_folder_path"] = check_for_attribute(lib, "root_folder_path", parent="sonarr", default=self.general["sonarr"]["root_folder_path"], req_default=True, save=False) sonarr_params["monitor"] = check_for_attribute(lib, "monitor", parent="sonarr", test_list=sonarr_monitors, default=self.general["sonarr"]["monitor"], save=False) sonarr_params["quality_profile"] = check_for_attribute(lib, "quality_profile", parent="sonarr", default=self.general["sonarr"]["quality_profile"], req_default=True, save=False) if self.general["sonarr"]["language_profile"]: sonarr_params["language_profile"] = check_for_attribute(lib, "language_profile", parent="sonarr", default=self.general["sonarr"]["language_profile"], save=False) else: sonarr_params["language_profile"] = check_for_attribute(lib, "language_profile", parent="sonarr", default_is_none=True, save=False) sonarr_params["series_type"] = check_for_attribute(lib, "series_type", parent="sonarr", test_list=sonarr_series_types, default=self.general["sonarr"]["series_type"], save=False) sonarr_params["season_folder"] = check_for_attribute(lib, "season_folder", parent="sonarr", var_type="bool", default=self.general["sonarr"]["season_folder"], save=False) sonarr_params["tag"] = check_for_attribute(lib, "search", parent="sonarr", var_type="lower_list", default=self.general["sonarr"]["tag"], default_is_none=True, save=False) sonarr_params["search"] = check_for_attribute(lib, "search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["search"], save=False) sonarr_params["cutoff_search"] = check_for_attribute(lib, "cutoff_search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["cutoff_search"], save=False) library.Sonarr = SonarrAPI(sonarr_params, library.Plex.language) except Failed as e: util.print_multiline(e, error=True) logger.info(f"{params['name']} library's Sonarr Connection {'Failed' if library.Sonarr is None else 'Successful'}") if self.general["tautulli"]["url"] or (lib and "tautulli" in lib): logger.info("") logger.info(f"Connecting to {params['name']} library's Tautulli...") tautulli_params = {} try: tautulli_params["url"] = check_for_attribute(lib, "url", parent="tautulli", default=self.general["tautulli"]["url"], req_default=True, save=False) tautulli_params["apikey"] = check_for_attribute(lib, "apikey", parent="tautulli", default=self.general["tautulli"]["apikey"], req_default=True, save=False) library.Tautulli = TautulliAPI(tautulli_params) except Failed as e: util.print_multiline(e, error=True) logger.info(f"{params['name']} library's Tautulli Connection {'Failed' if library.Tautulli is None else 'Successful'}") logger.info("") self.libraries.append(library) util.separator() if len(self.libraries) > 0: logger.info(f"{len(self.libraries)} Plex Library Connection{'s' if len(self.libraries) > 1 else ''} Successful") else: raise Failed("Plex Error: No Plex libraries were connected to") util.separator() def update_libraries(self, test, requested_collections, resume_from): for library in self.libraries: os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(library.timeout) logger.info("") util.separator(f"{library.name} Library") logger.info("") util.separator(f"Mapping {library.name} Library") logger.info("") movie_map, show_map = self.map_guids(library) if not test and not resume_from and library.mass_update: self.mass_metadata(library, movie_map, show_map) for metadata in library.metadata_files: logger.info("") util.separator(f"Running Metadata File\n{metadata.path}") if not test and not resume_from: try: metadata.update_metadata(self.TMDb, test) except Failed as e: logger.error(e) logger.info("") util.separator(f"{'Test ' if test else ''}Collections") collections = metadata.get_collections(requested_collections) if resume_from and resume_from not in collections: logger.warning(f"Collection: {resume_from} not in Metadata File: {metadata.path}") continue if collections: resume_from = self.run_collection(library, metadata, collections, test, resume_from, movie_map, show_map) if library.show_unmanaged is True and not test and not requested_collections: logger.info("") util.separator(f"Unmanaged Collections in {library.name} Library") logger.info("") unmanaged_count = 0 collections_in_plex = [str(plex_col) for plex_col in library.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)) if library.assets_for_all is True and not test and not requested_collections: logger.info("") util.separator(f"All {'Movies' if library.is_movie else 'Shows'} Assets Check for {library.name} Library") logger.info("") for item in library.get_all(): library.update_item_from_assets(item) has_run_again = False for library in self.libraries: if library.run_again: has_run_again = True break if has_run_again: logger.info("") util.separator("Run Again") logger.info("") length = 0 for x in range(1, self.general["run_again_delay"] + 1): length = util.print_return(length, f"Waiting to run again in {self.general['run_again_delay'] - x + 1} minutes") for y in range(60): time.sleep(1) util.print_end(length) for library in self.libraries: if library.run_again: os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(library.timeout) logger.info("") util.separator(f"{library.name} Library Run Again") logger.info("") movie_map, show_map = self.map_guids(library) for builder in library.run_again: logger.info("") util.separator(f"{builder.name} Collection") logger.info("") try: collection_obj = library.get_collection(builder.name) except Failed as e: util.print_multiline(e, error=True) continue builder.run_collections_again(collection_obj, movie_map, show_map) def run_collection(self, library, metadata, collections, test, resume_from, movie_map, show_map): for mapping_name, collection_attrs in collections.items(): if test and ("test" not in collection_attrs or collection_attrs["test"] is not True): no_template_test = True if "template" in collection_attrs and collection_attrs["template"]: for data_template in util.get_list(collection_attrs["template"], split=False): if "name" in data_template \ and data_template["name"] \ and metadata.templates \ and data_template["name"] in metadata.templates \ and metadata.templates[data_template["name"]] \ and "test" in metadata.templates[data_template["name"]] \ and metadata.templates[data_template["name"]]["test"] is True: no_template_test = False if no_template_test: continue try: if resume_from and resume_from != mapping_name: continue elif resume_from == mapping_name: resume_from = None logger.info("") util.separator(f"Resuming Collections") logger.info("") util.separator(f"{mapping_name} Collection") logger.info("") try: builder = CollectionBuilder(self, library, metadata, mapping_name, collection_attrs) except Failed as f: util.print_stacktrace() util.print_multiline(f, error=True) continue except Exception as e: util.print_stacktrace() logger.error(e) continue try: collection_obj = library.get_collection(mapping_name) collection_name = collection_obj.title collection_smart = library.smart(collection_obj) if (builder.smart and not collection_smart) or (not builder.smart and collection_smart): logger.info("") logger.error(f"Collection Error: Converting {collection_obj.title} to a {'smart' if builder.smart else 'normal'} collection") library.query(collection_obj.delete) collection_obj = None except Failed: collection_obj = None collection_name = mapping_name if len(builder.schedule) > 0: util.print_multiline(builder.schedule, info=True) rating_key_map = {} logger.info("") if builder.sync: logger.info("Sync Mode: sync") if collection_obj: for item in library.get_collection_items(collection_obj, builder.smart_label_collection): rating_key_map[item.ratingKey] = item else: logger.info("Sync Mode: append") for i, f in enumerate(builder.filters): if i == 0: logger.info("") logger.info(f"Collection Filter {f[0]}: {f[1]}") if not builder.smart_url: builder.run_methods(collection_obj, collection_name, rating_key_map, movie_map, show_map) try: if not collection_obj and builder.smart_url: library.create_smart_collection(collection_name, builder.smart_type_key, builder.smart_url) elif not collection_obj and builder.smart_label_collection: library.create_smart_labels(collection_name, sort=builder.smart_sort) plex_collection = library.get_collection(collection_name) except Failed as e: util.print_stacktrace() logger.error(e) continue builder.update_details(plex_collection) if builder.run_again and (len(builder.missing_movies) > 0 or len(builder.missing_shows) > 0): library.run_again.append(builder) except Exception as e: util.print_stacktrace() logger.error(f"Unknown Error: {e}") return resume_from def mass_metadata(self, library, movie_map, show_map): length = 0 logger.info("") util.separator(f"Mass Editing {'Movie' if library.is_movie else 'Show'} Library: {library.name}") logger.info("") items = library.Plex.all() for i, item in enumerate(items, 1): length = util.print_return(length, f"Processing: {i}/{len(items)} {item.title}") ids = {} if self.Cache: ids, expired = self.Cache.get_ids("movie" if library.is_movie else "show", plex_guid=item.guid) elif library.is_movie: for tmdb, rating_keys in movie_map.items(): if item.ratingKey in rating_keys: ids["tmdb"] = tmdb break else: for tvdb, rating_keys in show_map.items(): if item.ratingKey in rating_keys: ids["tvdb"] = tvdb break if library.mass_genre_update: if library.mass_genre_update == "tmdb": if "tmdb" not in ids: util.print_end(length, f"{item.title[:25]:<25} | No TMDb for Guid: {item.guid}") continue try: tmdb_item = self.TMDb.get_movie(ids["tmdb"]) if library.is_movie else self.TMDb.get_show(ids["tmdb"]) except Failed as e: util.print_end(length, str(e)) continue new_genres = [genre.name for genre in tmdb_item.genres] elif library.mass_genre_update in ["omdb", "imdb"]: if self.OMDb.limit is True: break if "imdb" not in ids: util.print_end(length, f"{item.title[:25]:<25} | No IMDb for Guid: {item.guid}") continue try: omdb_item = self.OMDb.get_omdb(ids["imdb"]) except Failed as e: util.print_end(length, str(e)) continue new_genres = omdb_item.genres else: raise Failed item_genres = [genre.tag for genre in item.genres] display_str = "" for genre in (g for g in item_genres if g not in new_genres): library.query_data(item.removeGenre, genre) display_str += f"{', ' if len(display_str) > 0 else ''}-{genre}" for genre in (g for g in new_genres if g not in item_genres): library.query_data(item.addGenre, genre) display_str += f"{', ' if len(display_str) > 0 else ''}+{genre}" if len(display_str) > 0: util.print_end(length, f"{item.title[:25]:<25} | Genres | {display_str}") if library.mass_audience_rating_update: if library.mass_audience_rating_update == "tmdb": if "tmdb" not in ids: util.print_end(length, f"{item.title[:25]:<25} | No TMDb for Guid: {item.guid}") continue try: tmdb_item = self.TMDb.get_movie(ids["tmdb"]) if library.is_movie else self.TMDb.get_show(ids["tmdb"]) except Failed as e: util.print_end(length, str(e)) continue new_rating = tmdb_item.vote_average elif library.mass_audience_rating_update in ["omdb", "imdb"]: if self.OMDb.limit is True: break if "imdb" not in ids: util.print_end(length, f"{item.title[:25]:<25} | No IMDb for Guid: {item.guid}") continue try: omdb_item = self.OMDb.get_omdb(ids["imdb"]) except Failed as e: util.print_end(length, str(e)) continue new_rating = omdb_item.imdb_rating else: raise Failed if new_rating is None: util.print_end(length, f"{item.title[:25]:<25} | No Rating Found") elif str(item.audienceRating) != str(new_rating): library.edit_query(item, {"audienceRating.value": new_rating, "audienceRating.locked": 1}) util.print_end(length, f"{item.title[:25]:<25} | Audience Rating | {new_rating}") def map_guids(self, library): movie_map = {} show_map = {} length = 0 logger.info(f"Mapping {'Movie' if library.is_movie else 'Show'} Library: {library.name}") items = library.Plex.all() for i, item in enumerate(items, 1): length = util.print_return(length, f"Processing: {i}/{len(items)} {item.title}") try: id_type, main_id = self.Arms.get_id(item, library, length) except BadRequest: util.print_stacktrace() util.print_end(length, f"{'Cache | ! |' if self.Cache else 'Mapping Error:'} | {item.guid} for {item.title} not found") continue if not isinstance(main_id, list): main_id = [main_id] if id_type == "movie": for m in main_id: if m in movie_map: movie_map[m].append(item.ratingKey) else: movie_map[m] = [item.ratingKey] elif id_type == "show": for m in main_id: if m in show_map: show_map[m].append(item.ratingKey) else: show_map[m] = [item.ratingKey] util.print_end(length, f"Processed {len(items)} {'Movies' if library.is_movie else 'Shows'}") return movie_map, show_map