diff --git a/modules/builder.py b/modules/builder.py index c2a923d8..ad92c9fc 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -245,119 +245,10 @@ class CollectionBuilder: if "template" in methods: logger.debug("") logger.debug("Validating Method: template") - if not self.metadata.templates: - raise Failed(f"{self.Type} Error: No templates found") - elif not self.data[methods["template"]]: - raise Failed(f"{self.Type} Error: template attribute is blank") - else: - logger.debug(f"Value: {self.data[methods['template']]}") - for variables in util.get_list(self.data[methods["template"]], split=False): - if not isinstance(variables, dict): - raise Failed(f"{self.Type} Error: template attribute is not a dictionary") - elif "name" not in variables: - raise Failed(f"{self.Type} Error: template sub-attribute name is required") - elif not variables["name"]: - raise Failed(f"{self.Type} Error: template sub-attribute name is blank") - elif variables["name"] not in self.metadata.templates: - raise Failed(f"{self.Type} Error: template {variables['name']} not found") - elif not isinstance(self.metadata.templates[variables["name"]], dict): - raise Failed(f"{self.Type} Error: template {variables['name']} is not a dictionary") - else: - for tm in variables: - if not variables[tm]: - raise Failed(f"{self.Type} Error: template sub-attribute {tm} is blank") - if "collection_name" not in variables: - variables["collection_name"] = str(self.name) - - template_name = variables["name"] - template = self.metadata.templates[template_name] - - default = {} - if "default" in template: - if template["default"]: - if isinstance(template["default"], dict): - for dv in template["default"]: - if template["default"][dv]: - default[dv] = template["default"][dv] - else: - raise Failed(f"{self.Type} Error: template default sub-attribute {dv} is blank") - else: - raise Failed(f"{self.Type} Error: template sub-attribute default is not a dictionary") - else: - raise Failed(f"{self.Type} Error: template sub-attribute default is blank") - - optional = [] - if "optional" in template: - if template["optional"]: - for op in util.get_list(template["optional"]): - if op not in default: - optional.append(str(op)) - else: - logger.warning(f"Template Warning: variable {op} cannot be optional if it has a default") - else: - raise Failed(f"{self.Type} Error: template sub-attribute optional is blank") - - if "move_collection_prefix" in template: - if template["move_collection_prefix"]: - for op in util.get_list(template["move_collection_prefix"]): - variables["collection_name"] = variables["collection_name"].replace(f"{str(op).strip()} ", "") + f", {str(op).strip()}" - else: - raise Failed(f"{self.Type} Error: template sub-attribute move_collection_prefix is blank") - - def check_data(_data): - if isinstance(_data, dict): - final_data = {} - for sm, sd in _data.items(): - try: - final_data[sm] = check_data(sd) - except Failed: - continue - elif isinstance(_data, list): - final_data = [] - for li in _data: - try: - final_data.append(check_data(li)) - except Failed: - continue - else: - txt = str(_data) - def scan_text(og_txt, var, var_value): - if og_txt == f"<<{var}>>": - return str(var_value) - elif f"<<{var}>>" in str(og_txt): - return str(og_txt).replace(f"<<{var}>>", str(var_value)) - else: - return og_txt - for option in optional: - if option not in variables and f"<<{option}>>" in txt: - raise Failed - for variable, variable_data in variables.items(): - if variable != "name": - txt = scan_text(txt, variable, variable_data) - for dm, dd in default.items(): - txt = scan_text(txt, dm, dd) - if txt in ["true", "True"]: - final_data = True - elif txt in ["false", "False"]: - final_data = False - else: - try: - num_data = float(txt) - final_data = int(num_data) if num_data.is_integer() else num_data - except (ValueError, TypeError): - final_data = txt - return final_data - - for method_name, attr_data in template.items(): - if method_name not in self.data and method_name not in ["default", "optional", "move_collection_prefix"]: - if attr_data is None: - logger.error(f"Template Error: template attribute {method_name} is blank") - continue - try: - self.data[method_name] = check_data(attr_data) - methods[method_name.lower()] = method_name - except Failed: - continue + new_attributes = self.metadata.apply_template(self.name, self.data, self.data[methods["template"]]) + for attr in new_attributes: + self.data[attr] = new_attributes[attr] + methods[attr.lower()] = attr if "delete_not_scheduled" in methods: logger.debug("") @@ -439,19 +330,6 @@ class CollectionBuilder: else: raise Failed(f"{self.Type} Error: {self.data[methods['collection_order']]} collection_order invalid\n\trelease (Order Collection by release dates)\n\talpha (Order Collection Alphabetically)\n\tcustom (Custom Order Collection)\n\tOther sorting options can be found at https://github.com/meisnate12/Plex-Meta-Manager/wiki/Smart-Builders#sort-options") - self.sort_by = None - if "sort_by" in methods and not self.playlist: - logger.debug("") - logger.debug("Validating Method: sort_by") - if self.data[methods["sort_by"]] is None: - raise Failed(f"{self.Type} Error: sort_by attribute is blank") - else: - logger.debug(f"Value: {self.data[methods['sort_by']]}") - if (self.library.is_movie and self.data[methods["sort_by"]] not in plex.movie_sorts) or (self.library.is_show and self.data[methods["sort_by"]] not in plex.show_sorts): - raise Failed(f"{self.Type} Error: sort_by attribute {self.data[methods['sort_by']]} invalid") - else: - self.sort_by = self.data[methods["sort_by"]] - self.collection_level = "movie" if self.library.is_movie else "show" if self.playlist: self.collection_level = "item" diff --git a/modules/meta.py b/modules/meta.py index ba17bbe0..57721dfe 100644 --- a/modules/meta.py +++ b/modules/meta.py @@ -9,65 +9,205 @@ logger = logging.getLogger("Plex Meta Manager") github_base = "https://raw.githubusercontent.com/meisnate12/Plex-Meta-Manager-Configs/master/" -class MetadataFile: - def __init__(self, config, library, file_type, path): + +def get_dict(attribute, attr_data, check_list=None): + if check_list is None: + check_list = [] + if attr_data and attribute in attr_data: + if attr_data[attribute]: + if isinstance(attr_data[attribute], dict): + new_dict = {} + for _name, _data in attr_data[attribute].items(): + if _name in check_list: + logger.error( + f"Config Warning: Skipping duplicate {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name}") + elif _data is None: + logger.error( + f"Config Warning: {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name} has no data") + elif not isinstance(_data, dict): + logger.error( + f"Config Warning: {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name} must be a dictionary") + else: + new_dict[str(_name)] = _data + return new_dict + else: + logger.warning(f"Config Warning: {attribute} must be a dictionary") + else: + logger.warning(f"Config Warning: {attribute} attribute is blank") + return None + + +class DataFile: + def __init__(self, config, file_type, path): self.config = config - self.library = library self.type = file_type self.path = path - def get_dict(attribute, attr_data, check_list=None): - if check_list is None: - check_list = [] - if attr_data and attribute in attr_data: - if attr_data[attribute]: - if isinstance(attr_data[attribute], dict): - new_dict = {} - for _name, _data in attr_data[attribute].items(): - if _name in check_list: - logger.error(f"Config Warning: Skipping duplicate {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name}") - elif _data is None: - logger.error(f"Config Warning: {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name} has no data") - elif not isinstance(_data, dict): - logger.error(f"Config Warning: {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name} must be a dictionary") - else: - new_dict[str(_name)] = _data - return new_dict - else: - logger.warning(f"Config Warning: {attribute} must be a dictionary") + self.data_type = "" + self.templates = {} + + def load_file(self): + try: + if self.type in ["URL", "Git"]: + content_path = self.path if self.type == "URL" else f"{github_base}{self.path}.yml" + response = self.config.get(content_path) + if response.status_code >= 400: + raise Failed(f"URL Error: No file found at {content_path}") + content = response.content + elif os.path.exists(os.path.abspath(self.path)): + content = open(self.path, encoding="utf-8") + else: + raise Failed(f"File Error: File does not exist {self.path}") + data, _, _ = yaml.util.load_yaml_guess_indent(content) + return data + except yaml.scanner.ScannerError as ye: + raise Failed(f"YAML Error: {util.tab_new_lines(ye)}") + except Exception as e: + util.print_stacktrace() + raise Failed(f"YAML Error: {e}") + + def apply_template(self, name, data, template): + if not self.templates: + raise Failed(f"{self.data_type} Error: No templates found") + elif not template: + raise Failed(f"{self.data_type} Error: template attribute is blank") + else: + logger.debug(f"Value: {template}") + for variables in util.get_list(template, split=False): + if not isinstance(variables, dict): + raise Failed(f"{self.data_type} Error: template attribute is not a dictionary") + elif "name" not in variables: + raise Failed(f"{self.data_type} Error: template sub-attribute name is required") + elif not variables["name"]: + raise Failed(f"{self.data_type} Error: template sub-attribute name is blank") + elif variables["name"] not in self.templates: + raise Failed(f"{self.data_type} Error: template {variables['name']} not found") + elif not isinstance(self.templates[variables["name"]], dict): + raise Failed(f"{self.data_type} Error: template {variables['name']} is not a dictionary") else: - logger.warning(f"Config Warning: {attribute} attribute is blank") - return None + for tm in variables: + if not variables[tm]: + raise Failed(f"{self.data_type} Error: template sub-attribute {tm} is blank") + if self.data_type == "Collection" and "collection_name" not in variables: + variables["collection_name"] = str(name) + if self.data_type == "Playlist" and "playlist_name" not in variables: + variables["playlist_name"] = str(name) + + template_name = variables["name"] + template = self.templates[template_name] + + default = {} + if "default" in template: + if template["default"]: + if isinstance(template["default"], dict): + for dv in template["default"]: + if template["default"][dv]: + default[dv] = template["default"][dv] + else: + raise Failed(f"{self.data_type} Error: template default sub-attribute {dv} is blank") + else: + raise Failed(f"{self.data_type} Error: template sub-attribute default is not a dictionary") + else: + raise Failed(f"{self.data_type} Error: template sub-attribute default is blank") + + optional = [] + if "optional" in template: + if template["optional"]: + for op in util.get_list(template["optional"]): + if op not in default: + optional.append(str(op)) + else: + logger.warning(f"Template Warning: variable {op} cannot be optional if it has a default") + else: + raise Failed(f"{self.data_type} Error: template sub-attribute optional is blank") + + if "move_collection_prefix" in template: + if template["move_collection_prefix"]: + for op in util.get_list(template["move_collection_prefix"]): + variables["collection_name"] = variables["collection_name"].replace(f"{str(op).strip()} ", "") + f", {str(op).strip()}" + else: + raise Failed(f"{self.data_type} Error: template sub-attribute move_collection_prefix is blank") + + def check_data(_method, _data): + if isinstance(_data, dict): + final_data = {} + for sm, sd in _data.items(): + try: + final_data[sm] = check_data(_method, sd) + except Failed: + continue + elif isinstance(_data, list): + final_data = [] + for li in _data: + try: + final_data.append(check_data(_method, li)) + except Failed: + continue + else: + txt = str(_data) + + def scan_text(og_txt, var, var_value): + if og_txt == f"<<{var}>>": + return str(var_value) + elif f"<<{var}>>" in str(og_txt): + return str(og_txt).replace(f"<<{var}>>", str(var_value)) + else: + return og_txt + + for option in optional: + if option not in variables and f"<<{option}>>" in txt: + raise Failed + for variable, variable_data in variables.items(): + if (variable == "collection_name" or variable == "playlist_name") and _method in ["radarr_tag", "item_radarr_tag", "sonarr_tag", "item_sonarr_tag"]: + txt = scan_text(txt, variable, variable_data.replace(",", "")) + elif variable != "name": + txt = scan_text(txt, variable, variable_data) + for dm, dd in default.items(): + txt = scan_text(txt, dm, dd) + if txt in ["true", "True"]: + final_data = True + elif txt in ["false", "False"]: + final_data = False + else: + try: + num_data = float(txt) + final_data = int(num_data) if num_data.is_integer() else num_data + except (ValueError, TypeError): + final_data = txt + return final_data + + new_attributes = {} + for method_name, attr_data in template.items(): + if method_name not in data and method_name not in ["default", "optional", "move_collection_prefix"]: + if attr_data is None: + logger.error(f"Template Error: template attribute {method_name} is blank") + continue + try: + new_attributes[method_name] = check_data(method_name, attr_data) + except Failed: + continue + return new_attributes + + +class MetadataFile(DataFile): + def __init__(self, config, library, file_type, path): + super().__init__(config, file_type, path) + self.data_type = "Collection" + self.library = library if file_type == "Data": self.metadata = None self.collections = get_dict("collections", path, library.collections) self.templates = get_dict("templates", path) else: - try: - logger.info("") - logger.info(f"Loading Metadata {file_type}: {path}") - if file_type in ["URL", "Git"]: - content_path = path if file_type == "URL" else f"{github_base}{path}.yml" - response = self.config.get(content_path) - if response.status_code >= 400: - raise Failed(f"URL Error: No file found at {content_path}") - content = response.content - elif os.path.exists(os.path.abspath(path)): - content = open(path, encoding="utf-8") - else: - raise Failed(f"File Error: File does not exist {path}") - data, ind, bsi = yaml.util.load_yaml_guess_indent(content) - self.metadata = get_dict("metadata", data, library.metadatas) - self.templates = get_dict("templates", data) - self.collections = get_dict("collections", data, library.collections) - - if self.metadata is None and self.collections is None: - raise Failed("YAML Error: metadata or collections attribute is required") - logger.info(f"Metadata File Loaded Successfully") - except yaml.scanner.ScannerError as ye: - raise Failed(f"YAML Error: {util.tab_new_lines(ye)}") - except Exception as e: - util.print_stacktrace() - raise Failed(f"YAML Error: {e}") + logger.info("") + logger.info(f"Loading Metadata {file_type}: {path}") + data = self.load_file() + self.metadata = get_dict("metadata", data, library.metadatas) + self.templates = get_dict("templates", data) + self.collections = get_dict("collections", data, library.collections) + + if self.metadata is None and self.collections is None: + raise Failed("YAML Error: metadata or collections attribute is required") + logger.info(f"Metadata File Loaded Successfully") def get_collections(self, requested_collections): if requested_collections: @@ -403,3 +543,18 @@ class MetadataFile: logger.error("Metadata Error: episodes attribute is blank") elif "episodes" in methods: logger.error("Metadata Error: episodes attribute only works for show libraries") + + +class PlaylistFile(DataFile): + def __init__(self, config, file_type, path): + super().__init__(config, file_type, path) + self.data_type = "Playlist" + self.playlists = {} + logger.info("") + logger.info(f"Loading Playlist File {file_type}: {path}") + data = self.load_file() + self.playlists = get_dict("playlists", data, self.config.playlist_names) + self.templates = get_dict("templates", data) + if not self.playlists: + raise Failed("YAML Error: playlists attribute is required") + logger.info(f"Playlist File Loaded Successfully") diff --git a/modules/playlist.py b/modules/playlist.py deleted file mode 100644 index 352bb1d6..00000000 --- a/modules/playlist.py +++ /dev/null @@ -1,66 +0,0 @@ -import logging, os, re -from datetime import datetime -from modules import plex, util -from modules.util import Failed, ImageData -from plexapi.exceptions import NotFound -from ruamel import yaml - -logger = logging.getLogger("Plex Meta Manager") - -github_base = "https://raw.githubusercontent.com/meisnate12/Plex-Meta-Manager-Configs/master/" - -class PlaylistFile: - def __init__(self, config, file_type, path): - self.config = config - self.type = file_type - self.path = path - self.playlists = {} - self.templates = {} - try: - logger.info("") - logger.info(f"Loading Playlist File {file_type}: {path}") - if file_type in ["URL", "Git"]: - content_path = path if file_type == "URL" else f"{github_base}{path}.yml" - response = self.config.get(content_path) - if response.status_code >= 400: - raise Failed(f"URL Error: No file found at {content_path}") - content = response.content - elif os.path.exists(os.path.abspath(path)): - content = open(path, encoding="utf-8") - else: - raise Failed(f"File Error: File does not exist {path}") - data, ind, bsi = yaml.util.load_yaml_guess_indent(content) - if data and "playlists" in data: - if data["playlists"]: - if isinstance(data["playlists"], dict): - for _name, _data in data["playlists"].items(): - if _name in self.config.playlist_names: - logger.error(f"Config Warning: Skipping duplicate playlist: {_name}") - elif _data is None: - logger.error(f"Config Warning: playlist: {_name} has no data") - elif not isinstance(_data, dict): - logger.error(f"Config Warning: playlist: {_name} must be a dictionary") - else: - self.playlists[str(_name)] = _data - else: - logger.warning(f"Config Warning: playlists must be a dictionary") - else: - logger.warning(f"Config Warning: playlists attribute is blank") - if not self.playlists: - raise Failed("YAML Error: playlists attribute is required") - if data and "templates" in data: - if data["templates"]: - if isinstance(data["templates"], dict): - for _name, _data in data["templates"].items(): - self.templates[str(_name)] = _data - else: - logger.warning(f"Config Warning: templates must be a dictionary") - else: - logger.warning(f"Config Warning: templates attribute is blank") - - logger.info(f"Playlist File Loaded Successfully") - except yaml.scanner.ScannerError as ye: - raise Failed(f"YAML Error: {util.tab_new_lines(ye)}") - except Exception as e: - util.print_stacktrace() - raise Failed(f"YAML Error: {e}")