Пример #1
0
 def edit_tags(attr,
               obj,
               group,
               alias,
               extra=None,
               movie_library=False):
     if movie_library and not self.library.is_movie and (
             attr in alias or f"{attr}.sync" in alias
             or f"{attr}.remove" in alias):
         logger.error(
             f"Metadata Error: {attr} attribute only works for movie libraries"
         )
     elif attr in alias and f"{attr}.sync" in alias:
         logger.error(
             f"Metadata Error: Cannot use {attr} and {attr}.sync together"
         )
     elif f"{attr}.remove" in alias and f"{attr}.sync" in alias:
         logger.error(
             f"Metadata Error: Cannot use {attr}.remove and {attr}.sync together"
         )
     elif attr in alias and group[alias[attr]] is None:
         logger.error(f"Metadata Error: {attr} attribute is blank")
     elif f"{attr}.remove" in alias and group[
             alias[f"{attr}.remove"]] is None:
         logger.error(
             f"Metadata Error: {attr}.remove attribute is blank")
     elif f"{attr}.sync" in alias and group[
             alias[f"{attr}.sync"]] is None:
         logger.error(
             f"Metadata Error: {attr}.sync attribute is blank")
     elif attr in alias or f"{attr}.remove" in alias or f"{attr}.sync" in alias:
         add_tags = util.get_list(
             group[alias[attr]]) if attr in alias else []
         if extra:
             add_tags.extend(extra)
         remove_tags = util.get_list(
             group[alias[f"{attr}.remove"]]
         ) if f"{attr}.remove" in alias else None
         sync_tags = util.get_list(
             group[alias[f"{attr}.sync"]]
             if group[alias[f"{attr}.sync"]] else []
         ) if f"{attr}.sync" in alias else None
         return self.library.edit_tags(attr,
                                       obj,
                                       add_tags=add_tags,
                                       remove_tags=remove_tags,
                                       sync_tags=sync_tags)
     return False
Пример #2
0
 def check_for_attribute(data, attribute, parent=None, test_list=None, options="", default=None, do_print=True, default_is_none=False, req_default=False, var_type="str", throw=False, save=True):
     message = ""
     endline = ""
     if parent is not None:
         if parent in data:
             data = data[parent]
         else:
             data = None
             do_print = False
             save = False
     text = "{} attribute".format(attribute) if parent is None else "{} sub-attribute {}".format(parent, attribute)
     if data is None or attribute not in data:
         message = "{} 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:
         if default_is_none is True:                                         return None
         else:                                                               message = "{} is blank".format(text)
     elif var_type == "bool":
         if isinstance(data[attribute], bool):                               return data[attribute]
         else:                                                               message = "{} 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 = "{} must an integer > 0".format(text)
     elif var_type == "path":
         if os.path.exists(os.path.abspath(data[attribute])):                return data[attribute]
         else:                                                               message = "Path {} does not exist".format(os.path.abspath(data[attribute]))
     elif var_type == "list":                                            return util.get_list(data[attribute])
     elif var_type == "listpath":
         temp_list = [path for path in util.get_list(data[attribute], split=True) if os.path.exists(os.path.abspath(path))]
         if len(temp_list) > 0:                                              return temp_list
         else:                                                               message = "No Paths exist"
     elif var_type == "lowerlist":                                       return util.get_list(data[attribute], lower=True)
     elif test_list is None or data[attribute] in test_list:             return data[attribute]
     else:                                                               message = "{}: {} is an invalid input".format(text, data[attribute])
     if var_type == "path" and default and os.path.exists(os.path.abspath(default)):
         return default
     elif var_type == "path" and default:
         default = None
         message = "neither {} or the default path {} could be found".format(data[attribute], default)
     if default is not None or default_is_none:
         message = message + " using {} as default".format(default)
     message = message + endline
     if req_default and default is None:
         raise Failed("Config Error: {} attribute must be set under {} globally or under this specific Library".format(attribute, parent))
     if (default is None and not default_is_none) or throw:
         if len(options) > 0:
             message = message + "\n" + options
         raise Failed("Config Error: {}".format(message))
     if do_print:
         util.print_multiline("Config Warning: {}".format(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
Пример #3
0
 def edit_tags(attr, obj, group, alias, key=None, extra=None, movie_library=False):
     if key is None:
         key = f"{attr}s"
     if attr in alias and f"{attr}.sync" in alias:
         logger.error(f"Metadata Error: Cannot use {attr} and {attr}.sync together")
     elif attr in alias or f"{attr}.sync" in alias:
         attr_key = attr if attr in alias else f"{attr}.sync"
         if movie_library and not self.is_movie:
             logger.error(f"Metadata Error: {attr_key} attribute only works for movie libraries")
         elif group[alias[attr_key]] or extra:
             item_tags = [item_tag.tag for item_tag in getattr(obj, key)]
             input_tags = []
             if group[alias[attr_key]]:
                 input_tags.extend(util.get_list(group[alias[attr_key]]))
             if extra:
                 input_tags.extend(extra)
             if f"{attr}.sync" in alias:
                 remove_method = getattr(obj, f"remove{attr.capitalize()}")
                 for tag in (t for t in item_tags if t not in input_tags):
                     updated = True
                     remove_method(tag)
                     logger.info(f"Detail: {attr.capitalize()} {tag} removed")
             add_method = getattr(obj, f"add{attr.capitalize()}")
             for tag in (t for t in input_tags if t not in item_tags):
                 updated = True
                 add_method(tag)
                 logger.info(f"Detail: {attr.capitalize()} {tag} added")
         else:
             logger.error(f"Metadata Error: {attr} attribute is blank")
Пример #4
0
 def __init__(self, imdb_id, data):
     self._imdb_id = imdb_id
     self._data = data
     if data["Response"] == "False":
         raise Failed(f"OMDb Error: {data['Error']} IMDb ID: {imdb_id}")
     self.title = data["Title"]
     try:
         self.year = int(data["Year"])
     except (ValueError, TypeError):
         self.year = None
     self.content_rating = data["Rated"]
     self.genres = util.get_list(data["Genre"])
     self.genres_str = data["Genre"]
     try:
         self.imdb_rating = float(data["imdbRating"])
     except (ValueError, TypeError):
         self.imdb_rating = None
     try:
         self.imdb_votes = int(str(data["imdbVotes"]).replace(',', ''))
     except (ValueError, TypeError):
         self.imdb_votes = None
     try:
         self.metacritic_rating = int(data["Metascore"])
     except (ValueError, TypeError):
         self.metacritic_rating = None
     self.imdb_id = data["imdbID"]
     self.type = data["Type"]
Пример #5
0
 def validate_imdb_lists(self, imdb_lists, language):
     valid_lists = []
     for imdb_dict in util.get_list(imdb_lists, split=False):
         if not isinstance(imdb_dict, dict):
             imdb_dict = {"url": imdb_dict}
         dict_methods = {dm.lower(): dm for dm in imdb_dict}
         imdb_url = util.parse("url",
                               imdb_dict,
                               methods=dict_methods,
                               parent="imdb_list").strip()
         if not imdb_url.startswith(tuple([v for k, v in urls.items()])):
             fails = "\n".join([
                 f"{v} (For {k.replace('_', ' ').title()})"
                 for k, v in urls.items()
             ])
             raise Failed(
                 f"IMDb Error: {imdb_url} must begin with either:{fails}")
         self._total(imdb_url, language)
         list_count = util.parse(
             "limit",
             imdb_dict,
             datatype="int",
             methods=dict_methods,
             default=0,
             parent="imdb_list",
             minimum=0) if "limit" in dict_methods else 0
         valid_lists.append({"url": imdb_url, "limit": list_count})
     return valid_lists
Пример #6
0
 def get_collections(self, requested_collections):
     if requested_collections:
         return {
             c: self.collections[c]
             for c in util.get_list(requested_collections)
             if c in self.collections
         }
     else:
         return self.collections
Пример #7
0
 def validate_search_list(self, data, search_name):
     final_search = util.search_alias[search_name] if search_name in util.search_alias else search_name
     search_choices = self.get_search_choices(final_search, key=final_search.endswith("Language"))
     valid_list = []
     for value in util.get_list(data):
         if str(value).lower() in search_choices:
             valid_list.append(search_choices[str(value).lower()])
         else:
             raise Failed(f"Plex Error: {search_name}: {value} not found")
     return valid_list
Пример #8
0
 def validate_search_list(self, data, search_name):
     final_search = search_translation[search_name] if search_name in search_translation else search_name
     search_choices = self.get_search_choices(final_search)
     valid_list = []
     for value in util.get_list(data):
         if str(value).lower() in search_choices:
             valid_list.append(search_choices[str(value).lower()])
         else:
             logger.error(f"Plex Error: {search_name}: {value} not found")
     return valid_list
Пример #9
0
 def query_guid_map(self, plex_guid):
     id_to_return = None
     imdb_id = None
     media_type = None
     expired = None
     with sqlite3.connect(self.cache_path) as connection:
         connection.row_factory = sqlite3.Row
         with closing(connection.cursor()) as cursor:
             cursor.execute(f"SELECT * FROM guids_map WHERE plex_guid = ?",
                            (plex_guid, ))
             row = cursor.fetchone()
             if row:
                 time_between_insertion = datetime.now(
                 ) - datetime.strptime(row["expiration_date"], "%Y-%m-%d")
                 id_to_return = util.get_list(row["t_id"], int_list=True)
                 imdb_id = util.get_list(row["imdb_id"])
                 media_type = row["media_type"]
                 expired = time_between_insertion.days > self.expiration
     return id_to_return, imdb_id, media_type, expired
Пример #10
0
 def validate(self, name, data):
     valid = []
     for d in util.get_list(data):
         if d.lower().replace(" / ",
                              "-").replace(" ", "-") in self.options[name]:
             valid.append(d)
     if len(valid) > 0:
         return valid
     raise Failed(
         f"AniList Error: {name}: {data} does not exist\nOptions: {', '.join([v for k, v in self.options[name].items()])}"
     )
Пример #11
0
 def validate_icheckmovies_lists(self, icheckmovies_lists, language):
     valid_lists = []
     for icheckmovies_list in util.get_list(icheckmovies_lists, split=False):
         list_url = icheckmovies_list.strip()
         if not list_url.startswith(base_url):
             raise Failed(f"ICheckMovies Error: {list_url} must begin with: {base_url}")
         elif len(self._parse_list(list_url, language)) > 0:
             valid_lists.append(list_url)
         else:
             raise Failed(f"ICheckMovies Error: {list_url} failed to parse")
     return valid_lists
Пример #12
0
 def validate_letterboxd_lists(self, letterboxd_lists, language):
     valid_lists = []
     for letterboxd_list in util.get_list(letterboxd_lists, split=False):
         list_url = letterboxd_list.strip()
         if not list_url.startswith(base_url):
             raise Failed(
                 f"Letterboxd Error: {list_url} must begin with: {base_url}"
             )
         elif len(self._parse_list(list_url, language)) > 0:
             valid_lists.append(list_url)
         else:
             raise Failed(f"Letterboxd Error: {list_url} failed to parse")
     return valid_lists
Пример #13
0
 def validate_flixpatrol_lists(self, flixpatrol_lists, language, is_movie):
     valid_lists = []
     for flixpatrol_list in util.get_list(flixpatrol_lists, split=False):
         list_url = flixpatrol_list.strip()
         if not list_url.startswith(tuple([v for k, v in urls.items()])):
             fails = "\n".join([
                 f"{v} (For {k.replace('_', ' ').title()})"
                 for k, v in urls.items()
             ])
             raise Failed(
                 f"FlixPatrol Error: {list_url} must begin with either:{fails}"
             )
         elif len(self._parse_list(list_url, language, is_movie)) > 0:
             valid_lists.append(list_url)
         else:
             raise Failed(f"FlixPatrol Error: {list_url} failed to parse")
     return valid_lists
Пример #14
0
 def __init__(self, config):
     self.config = config
     self.anidb_ids = {}
     self.mal_to_anidb = {}
     self.anilist_to_anidb = {}
     self.anidb_to_imdb = {}
     self.anidb_to_tvdb = {}
     for anime_id in self.config.get_json(anime_lists_url):
         if "anidb_id" in anime_id:
             self.anidb_ids[anime_id["anidb_id"]] = anime_id
             if "mal_id" in anime_id:
                 self.mal_to_anidb[int(anime_id["mal_id"])] = int(anime_id["anidb_id"])
             if "anilist_id" in anime_id:
                 self.anilist_to_anidb[int(anime_id["anilist_id"])] = int(anime_id["anidb_id"])
             if "imdb_id" in anime_id and str(anime_id["imdb_id"]).startswith("tt"):
                 self.anidb_to_imdb[int(anime_id["anidb_id"])] = util.get_list(anime_id["imdb_id"])
             if "thetvdb_id" in anime_id:
                 self.anidb_to_tvdb[int(anime_id["anidb_id"])] = int(anime_id["thetvdb_id"])
Пример #15
0
 def validate_imdb_lists(self, imdb_lists, language):
     valid_lists = []
     for imdb_dict in util.get_list(imdb_lists, split=False):
         if not isinstance(imdb_dict, dict):
             imdb_dict = {"url": imdb_dict}
         dict_methods = {dm.lower(): dm for dm in imdb_dict}
         if "url" not in dict_methods:
             raise Failed(
                 f"Collection Error: imdb_list url attribute not found")
         elif imdb_dict[dict_methods["url"]] is None:
             raise Failed(
                 f"Collection Error: imdb_list url attribute is blank")
         else:
             imdb_url = imdb_dict[dict_methods["url"]].strip()
         if not imdb_url.startswith(tuple([v for k, v in urls.items()])):
             fails = "\n".join([
                 f"{v} (For {k.replace('_', ' ').title()})"
                 for k, v in urls.items()
             ])
             raise Failed(
                 f"IMDb Error: {imdb_url} must begin with either:{fails}")
         self._total(imdb_url, language)
         list_count = None
         if "limit" in dict_methods:
             if imdb_dict[dict_methods["limit"]] is None:
                 logger.warning(
                     f"Collection Warning: imdb_list limit attribute is blank using 0 as default"
                 )
             else:
                 try:
                     value = int(str(imdb_dict[dict_methods["limit"]]))
                     if 0 <= value:
                         list_count = value
                 except ValueError:
                     pass
             if list_count is None:
                 logger.warning(
                     f"Collection Warning: imdb_list limit attribute must be an integer 0 or greater using 0 as default"
                 )
         if list_count is None:
             list_count = 0
         valid_lists.append({"url": imdb_url, "limit": list_count})
     return valid_lists
Пример #16
0
 def validate_mdblist_lists(self, error_type, mdb_lists):
     valid_lists = []
     for mdb_dict in util.get_list(mdb_lists, split=False):
         if not isinstance(mdb_dict, dict):
             mdb_dict = {"url": mdb_dict}
         dict_methods = {dm.lower(): dm for dm in mdb_dict}
         if "url" not in dict_methods:
             raise Failed(f"{error_type} Error: mdb_list url attribute not found")
         elif mdb_dict[dict_methods["url"]] is None:
             raise Failed(f"{error_type} Error: mdb_list url attribute is blank")
         else:
             mdb_url = mdb_dict[dict_methods["url"]].strip()
         if not mdb_url.startswith(base_url):
             raise Failed(f"{error_type} Error: {mdb_url} must begin with: {base_url}")
         list_count = None
         if "limit" in dict_methods:
             if mdb_dict[dict_methods["limit"]] is None:
                 logger.warning(f"{error_type} Warning: mdb_list limit attribute is blank using 0 as default")
             else:
                 try:
                     value = int(str(mdb_dict[dict_methods["limit"]]))
                     if 0 <= value:
                         list_count = value
                 except ValueError:
                     pass
             if list_count is None:
                 logger.warning(f"{error_type} Warning: mdb_list limit attribute must be an integer 0 or greater using 0 as default")
         if list_count is None:
             list_count = 0
         sort_by = "score.desc"
         if "sort_by" in dict_methods:
             if mdb_dict[dict_methods["sort_by"]] is None:
                 logger.warning(f"{error_type} Warning: mdb_list sort_by attribute is blank using score as default")
             elif mdb_dict[dict_methods["sort_by"]].lower() in sort_names:
                 logger.warning(f"{error_type} Warning: mdb_list sort_by attribute {mdb_dict[dict_methods['sort_by']]} is missing .desc or .asc defaulting to .desc")
                 sort_by = f"{mdb_dict[dict_methods['sort_by']].lower()}.desc"
             elif mdb_dict[dict_methods["sort_by"]].lower() not in list_sorts:
                 logger.warning(f"{error_type} Warning: mdb_list sort_by attribute {mdb_dict[dict_methods['sort_by']]} not valid score as default. Options: {', '.join(list_sorts)}")
             else:
                 sort_by = mdb_dict[dict_methods["sort_by"]].lower()
         valid_lists.append({"url": mdb_url, "limit": list_count, "sort_by": sort_by})
     return valid_lists
Пример #17
0
 def _load_anime_conversion(self):
     if not self._loaded:
         for anime_id in self.config.get_json(anime_lists_url):
             if "anidb_id" in anime_id:
                 self._anidb_ids[anime_id["anidb_id"]] = anime_id
                 if "mal_id" in anime_id:
                     self._mal_to_anidb[int(anime_id["mal_id"])] = int(
                         anime_id["anidb_id"])
                 if "anilist_id" in anime_id:
                     self._anilist_to_anidb[int(
                         anime_id["anilist_id"])] = int(
                             anime_id["anidb_id"])
                 if "imdb_id" in anime_id and str(
                         anime_id["imdb_id"]).startswith("tt"):
                     self._anidb_to_imdb[int(
                         anime_id["anidb_id"])] = util.get_list(
                             anime_id["imdb_id"])
                 if "thetvdb_id" in anime_id:
                     self._anidb_to_tvdb[int(anime_id["anidb_id"])] = int(
                         anime_id["thetvdb_id"])
         self._loaded = True
Пример #18
0
 def validate_trakt(self, trakt_lists, is_movie, trakt_type="list"):
     values = util.get_list(trakt_lists, split=False)
     trakt_values = []
     for value in values:
         if isinstance(value, dict):
             raise Failed("Trakt Error: List cannot be a dictionary")
         try:
             if trakt_type == "list":
                 self._user_list(value)
             else:
                 self._user_items(trakt_type, value, is_movie)
             trakt_values.append(value)
         except Failed as e:
             logger.error(e)
     if len(trakt_values) == 0:
         if trakt_type == "watchlist":
             raise Failed(f"Trakt Error: No valid Trakt Watchlists in {values}")
         elif trakt_type == "collection":
             raise Failed(f"Trakt Error: No valid Trakt Collections in {values}")
         else:
             raise Failed(f"Trakt Error: No valid Trakt Lists in {values}")
     return trakt_values
Пример #19
0
 def __init__(self, data):
     self._data = data
     self.title = data["Title"]
     try:
         self.year = int(data["Year"])
     except (ValueError, TypeError):
         self.year = None
     self.content_rating = data["Rated"]
     self.genres = util.get_list(data["Genre"])
     self.genres_str = data["Genre"]
     try:
         self.imdb_rating = float(data["imdbRating"])
     except (ValueError, TypeError):
         self.imdb_rating = None
     try:
         self.imdb_votes = int(str(data["imdbVotes"]).replace(',', ''))
     except (ValueError, TypeError):
         self.imdb_votes = None
     try:
         self.metacritic_rating = int(data["Metascore"])
     except (ValueError, TypeError):
         self.metacritic_rating = None
     self.imdb_id = data["imdbID"]
     self.type = data["Type"]
        logger.info("")
        util.separator(f"Finished {mapping_name} Collection\nCollection Run Time: {str(datetime.now() - collection_start).split('.')[0]}")
        logger.removeHandler(collection_handler)

try:
    if run or test or collections or libraries or resume:
        start({
            "config_file": config_file,
            "test": test,
            "collections": collections,
            "libraries": libraries,
            "resume": resume,
            "trace": trace
        })
    else:
        times_to_run = util.get_list(times)
        valid_times = []
        for time_to_run in times_to_run:
            try:
                valid_times.append(datetime.strftime(datetime.strptime(time_to_run, "%H:%M"), "%H:%M"))
            except ValueError:
                if time_to_run:
                    raise Failed(f"Argument Error: time argument invalid: {time_to_run} must be in the HH:MM format")
                else:
                    raise Failed(f"Argument Error: blank time argument")
        for time_to_run in valid_times:
            schedule.every().day.at(time_to_run).do(start, {"config_file": config_file, "time": time_to_run, "trace": trace})
        while True:
            schedule.run_pending()
            if not no_countdown:
                current = datetime.now().strftime("%H:%M")
Пример #21
0
    def update_metadata(self, TMDb, test):
        logger.info("")
        util.separator(f"{self.name} Library Metadata")
        logger.info("")
        if not self.metadata:
            raise Failed("No metadata to edit")
        for m in self.metadata:
            if test and ("test" not in self.metadata[m]
                         or self.metadata[m]["test"] is not True):
                continue
            logger.info("")
            util.separator()
            logger.info("")
            year = None
            if "year" in self.metadata[m]:
                year = util.check_number(self.metadata[m]["year"],
                                         "year",
                                         minimum=1800,
                                         maximum=datetime.now().year + 1)

            title = m
            if "title" in self.metadata[m]:
                if self.metadata[m]["title"] is None:
                    logger.error("Metadata Error: title attribute is blank")
                else:
                    title = self.metadata[m]["title"]

            item = self.search_item(title, year=year)

            if item is None:
                item = self.search_item(f"{title} (SUB)", year=year)

            if item is None and "alt_title" in self.metadata[m]:
                if self.metadata[m]["alt_title"] is None:
                    logger.error(
                        "Metadata Error: alt_title attribute is blank")
                else:
                    alt_title = self.metadata[m]["alt_title"]
                    item = self.search_item(alt_title, year=year)

            if item is None:
                logger.error(f"Plex Error: Item {m} not found")
                logger.error(f"Skipping {m}")
                continue

            item_type = "Movie" if self.is_movie else "Show"
            logger.info(f"Updating {item_type}: {title}...")

            tmdb_item = None
            try:
                if "tmdb_id" in self.metadata[m]:
                    if self.metadata[m]["tmdb_id"] is None:
                        logger.error(
                            "Metadata Error: tmdb_id attribute is blank")
                    elif self.is_show:
                        logger.error(
                            "Metadata Error: tmdb_id attribute only works with movie libraries"
                        )
                    else:
                        tmdb_item = TMDb.get_show(
                            util.regex_first_int(self.metadata[m]["tmdb_id"],
                                                 "Show"))
            except Failed as e:
                logger.error(e)

            originally_available = tmdb_item.first_air_date if tmdb_item else None
            rating = tmdb_item.vote_average if tmdb_item else None
            original_title = tmdb_item.original_name if tmdb_item and tmdb_item.original_name != tmdb_item.name else None
            studio = tmdb_item.networks[0].name if tmdb_item else None
            tagline = tmdb_item.tagline if tmdb_item and len(
                tmdb_item.tagline) > 0 else None
            summary = tmdb_item.overview if tmdb_item else None

            edits = {}

            def add_edit(name, current, group, key=None, value=None):
                if value or name in group:
                    if value or group[name]:
                        if key is None: key = name
                        if value is None: value = group[name]
                        if str(current) != str(value):
                            edits[f"{key}.value"] = value
                            edits[f"{key}.locked"] = 1
                            logger.info(f"Detail: {name} updated to {value}")
                    else:
                        logger.error(
                            f"Metadata Error: {name} attribute is blank")

            add_edit("title", item.title, self.metadata[m], value=title)
            add_edit("sort_title",
                     item.titleSort,
                     self.metadata[m],
                     key="titleSort")
            add_edit("originally_available",
                     str(item.originallyAvailableAt)[:-9],
                     self.metadata[m],
                     key="originallyAvailableAt",
                     value=originally_available)
            add_edit("rating", item.rating, self.metadata[m], value=rating)
            add_edit("content_rating",
                     item.contentRating,
                     self.metadata[m],
                     key="contentRating")
            add_edit("original_title",
                     item.originalTitle,
                     self.metadata[m],
                     key="originalTitle",
                     value=original_title)
            add_edit("studio", item.studio, self.metadata[m], value=studio)
            add_edit("tagline", item.tagline, self.metadata[m], value=tagline)
            add_edit("summary", item.summary, self.metadata[m], value=summary)
            if len(edits) > 0:
                logger.debug(f"Details Update: {edits}")
                try:
                    item.edit(**edits)
                    item.reload()
                    logger.info(f"{item_type}: {m} Details Update Successful")
                except BadRequest:
                    util.print_stacktrace()
                    logger.error(f"{item_type}: {m} Details Update Failed")
            else:
                logger.info(f"{item_type}: {m} Details Update Not Needed")

            genres = []

            if tmdb_item:
                genres.extend([genre.name for genre in tmdb_item.genres])

            if "genre" in self.metadata[m]:
                if self.metadata[m]["genre"]:
                    genres.extend(util.get_list(self.metadata[m]["genre"]))
                else:
                    logger.error("Metadata Error: genre attribute is blank")

            if len(genres) > 0:
                item_genres = [genre.tag for genre in item.genres]
                if "genre_sync_mode" in self.metadata[m]:
                    if self.metadata[m]["genre_sync_mode"] is None:
                        logger.error(
                            "Metadata Error: genre_sync_mode attribute is blank defaulting to append"
                        )
                    elif self.metadata[m]["genre_sync_mode"] not in [
                            "append", "sync"
                    ]:
                        logger.error(
                            "Metadata Error: genre_sync_mode attribute must be either 'append' or 'sync' defaulting to append"
                        )
                    elif self.metadata[m]["genre_sync_mode"] == "sync":
                        for genre in (g for g in item_genres
                                      if g not in genres):
                            item.removeGenre(genre)
                            logger.info(f"Detail: Genre {genre} removed")
                for genre in (g for g in genres if g not in item_genres):
                    item.addGenre(genre)
                    logger.info(f"Detail: Genre {genre} added")

            if "label" in self.metadata[m]:
                if self.metadata[m]["label"]:
                    item_labels = [label.tag for label in item.labels]
                    labels = util.get_list(self.metadata[m]["label"])
                    if "label_sync_mode" in self.metadata[m]:
                        if self.metadata[m]["label_sync_mode"] is None:
                            logger.error(
                                "Metadata Error: label_sync_mode attribute is blank defaulting to append"
                            )
                        elif self.metadata[m]["label_sync_mode"] not in [
                                "append", "sync"
                        ]:
                            logger.error(
                                "Metadata Error: label_sync_mode attribute must be either 'append' or 'sync' defaulting to append"
                            )
                        elif self.metadata[m]["label_sync_mode"] == "sync":
                            for label in (la for la in item_labels
                                          if la not in labels):
                                item.removeLabel(label)
                                logger.info(f"Detail: Label {label} removed")
                    for label in (la for la in labels
                                  if la not in item_labels):
                        item.addLabel(label)
                        logger.info(f"Detail: Label {label} added")
                else:
                    logger.error("Metadata Error: label attribute is blank")

            if "seasons" in self.metadata[m] and self.is_show:
                if self.metadata[m]["seasons"]:
                    for season_id in self.metadata[m]["seasons"]:
                        logger.info("")
                        logger.info(f"Updating season {season_id} of {m}...")
                        if isinstance(season_id, int):
                            try:
                                season = item.season(season_id)
                            except NotFound:
                                logger.error(
                                    f"Metadata Error: Season: {season_id} not found"
                                )
                            else:

                                if "title" in self.metadata[m]["seasons"][
                                        season_id] and self.metadata[m][
                                            "seasons"][season_id]["title"]:
                                    title = self.metadata[m]["seasons"][
                                        season_id]["title"]
                                else:
                                    title = season.title
                                if "sub" in self.metadata[m]["seasons"][
                                        season_id]:
                                    if self.metadata[m]["seasons"][season_id][
                                            "sub"] is None:
                                        logger.error(
                                            "Metadata Error: sub attribute is blank"
                                        )
                                    elif self.metadata[m]["seasons"][season_id][
                                            "sub"] is True and "(SUB)" not in title:
                                        title = f"{title} (SUB)"
                                    elif self.metadata[m]["seasons"][season_id][
                                            "sub"] is False and title.endswith(
                                                " (SUB)"):
                                        title = title[:-6]
                                    else:
                                        logger.error(
                                            "Metadata Error: sub attribute must be True or False"
                                        )

                                edits = {}
                                add_edit(
                                    "title",
                                    season.title,
                                    self.metadata[m]["seasons"][season_id],
                                    value=title)
                                add_edit(
                                    "summary", season.summary,
                                    self.metadata[m]["seasons"][season_id])
                                if len(edits) > 0:
                                    logger.debug(
                                        f"Season: {season_id} Details Update: {edits}"
                                    )
                                    try:
                                        season.edit(**edits)
                                        season.reload()
                                        logger.info(
                                            f"Season: {season_id} Details Update Successful"
                                        )
                                    except BadRequest:
                                        util.print_stacktrace()
                                        logger.error(
                                            f"Season: {season_id} Details Update Failed"
                                        )
                                else:
                                    logger.info(
                                        f"Season: {season_id} Details Update Not Needed"
                                    )
                        else:
                            logger.error(
                                f"Metadata Error: Season: {season_id} invalid, it must be an integer"
                            )
                else:
                    logger.error("Metadata Error: seasons attribute is blank")

            if "episodes" in self.metadata[m] and self.is_show:
                if self.metadata[m]["episodes"]:
                    for episode_str in self.metadata[m]["episodes"]:
                        logger.info("")
                        match = re.search("[Ss]\\d+[Ee]\\d+", episode_str)
                        if match:
                            output = match.group(0)[1:].split(
                                "E" if "E" in m.group(0) else "e")
                            episode_id = int(output[0])
                            season_id = int(output[1])
                            logger.info(
                                f"Updating episode S{episode_id}E{season_id} of {m}..."
                            )
                            try:
                                episode = item.episode(season=season_id,
                                                       episode=episode_id)
                            except NotFound:
                                logger.error(
                                    f"Metadata Error: episode {episode_id} of season {season_id} not found"
                                )
                            else:
                                if "title" in self.metadata[m]["episodes"][
                                        episode_str] and self.metadata[m][
                                            "episodes"][episode_str]["title"]:
                                    title = self.metadata[m]["episodes"][
                                        episode_str]["title"]
                                else:
                                    title = episode.title
                                if "sub" in self.metadata[m]["episodes"][
                                        episode_str]:
                                    if self.metadata[m]["episodes"][
                                            episode_str]["sub"] is None:
                                        logger.error(
                                            "Metadata Error: sub attribute is blank"
                                        )
                                    elif self.metadata[m]["episodes"][episode_str][
                                            "sub"] is True and "(SUB)" not in title:
                                        title = f"{title} (SUB)"
                                    elif self.metadata[m]["episodes"][
                                            episode_str][
                                                "sub"] is False and title.endswith(
                                                    " (SUB)"):
                                        title = title[:-6]
                                    else:
                                        logger.error(
                                            "Metadata Error: sub attribute must be True or False"
                                        )
                                edits = {}
                                add_edit(
                                    "title",
                                    episode.title,
                                    self.metadata[m]["episodes"][episode_str],
                                    value=title)
                                add_edit(
                                    "sort_title",
                                    episode.titleSort,
                                    self.metadata[m]["episodes"][episode_str],
                                    key="titleSort")
                                add_edit(
                                    "rating", episode.rating,
                                    self.metadata[m]["episodes"][episode_str])
                                add_edit(
                                    "originally_available",
                                    str(episode.originallyAvailableAt)[:-9],
                                    self.metadata[m]["episodes"][episode_str],
                                    key="originallyAvailableAt")
                                add_edit(
                                    "summary", episode.summary,
                                    self.metadata[m]["episodes"][episode_str])
                                if len(edits) > 0:
                                    logger.debug(
                                        f"Season: {season_id} Episode: {episode_id} Details Update: {edits}"
                                    )
                                    try:
                                        episode.edit(**edits)
                                        episode.reload()
                                        logger.info(
                                            f"Season: {season_id} Episode: {episode_id} Details Update Successful"
                                        )
                                    except BadRequest:
                                        util.print_stacktrace()
                                        logger.error(
                                            f"Season: {season_id} Episode: {episode_id} Details Update Failed"
                                        )
                                else:
                                    logger.info(
                                        f"Season: {season_id} Episode: {episode_id} Details Update Not Needed"
                                    )
                        else:
                            logger.error(
                                f"Metadata Error: episode {episode_str} invalid must have S##E## format"
                            )
                else:
                    logger.error("Metadata Error: episodes attribute is blank")
Пример #22
0
    def update_metadata(self):
        logger.info("")
        util.seperator("{} Library Metadata".format(self.name))
        logger.info("")
        if not self.metadata:
            raise Failed("No metadata to edit")
        for m in self.metadata:
            logger.info("")
            util.seperator()
            logger.info("")
            year = None
            if "year" in self.metadata[m]:
                now = datetime.datetime.now()
                if self.metadata[m]["year"] is None:                                    logger.error("Metadata Error: year attribute is blank")
                elif not isinstance(self.metadata[m]["year"], int):                     logger.error("Metadata Error: year attribute must be an integer")
                elif self.metadata[m]["year"] not in range(1800, now.year + 2):         logger.error("Metadata Error: year attribute must be between 1800-{}".format(now.year + 1))
                else:                                                                   year = self.metadata[m]["year"]

            alt_title = None
            used_alt = False
            if "alt_title" in self.metadata[m]:
                if self.metadata[m]["alt_title"] is None:                               logger.error("Metadata Error: alt_title attribute is blank")
                else:                                                                   alt_title = self.metadata[m]["alt_title"]

            try:
                item = self.get_item(m, year=year)
            except Failed as e:
                if alt_title:
                    try:
                        item = self.get_item(alt_title, year=year)
                        used_alt = True
                    except Failed as alt_e:
                        logger.error(alt_e)
                        logger.error("Skipping {}".format(m))
                        continue
                else:
                    logger.error(e)
                    logger.error("Skipping {}".format(m))
                    continue

            logger.info("Updating {}: {}...".format("Movie" if self.is_movie else "Show", alt_title if used_alt else m))
            edits = {}
            def add_edit(name, group, key=None, value=None, sub=None):
                if value or name in group:
                    if value or group[name]:
                        if key is None:         key = name
                        if value is None:       value = group[name]
                        if sub and "sub" in group:
                            if group["sub"]:
                                if group["sub"] is True and "(SUB)" not in value:       value = "{} (SUB)".format(value)
                                elif group["sub"] is False and " (SUB)" in value:       value = value[:-6]
                            else:
                                logger.error("Metadata Error: sub attribute is blank")
                        edits["{}.value".format(key)] = value
                        edits["{}.locked".format(key)] = 1
                    else:
                        logger.error("Metadata Error: {} attribute is blank".format(name))
            if used_alt or "sub" in self.metadata[m]:
                add_edit("title", self.metadata[m], value=m, sub=True)
            add_edit("sort_title", self.metadata[m], key="titleSort")
            add_edit("originally_available", self.metadata[m], key="originallyAvailableAt")
            add_edit("rating", self.metadata[m])
            add_edit("content_rating", self.metadata[m], key="contentRating")
            add_edit("original_title", self.metadata[m], key="originalTitle")
            add_edit("studio", self.metadata[m])
            add_edit("tagline", self.metadata[m])
            add_edit("summary", self.metadata[m])
            try:
                item.edit(**edits)
                item.reload()
                logger.info("{}: {} Details Update Successful".format("Movie" if self.is_movie else "Show", m))
            except BadRequest:
                logger.error("{}: {} Details Update Failed".format("Movie" if self.is_movie else "Show", m))
                logger.debug("Details Update: {}".format(edits))
                util.print_stacktrace()

            if "genre" in self.metadata[m]:
                if self.metadata[m]["genre"]:
                    genre_sync = False
                    if "genre_sync_mode" in self.metadata[m]:
                        if self.metadata[m]["genre_sync_mode"] is None:                         logger.error("Metadata Error: genre_sync_mode attribute is blank defaulting to append")
                        elif self.metadata[m]["genre_sync_mode"] not in ["append", "sync"]:     logger.error("Metadata Error: genre_sync_mode attribute must be either 'append' or 'sync' defaulting to append")
                        elif self.metadata[m]["genre_sync_mode"] == "sync":                     genre_sync = True
                    genres = [genre.tag for genre in item.genres]
                    values = util.get_list(self.metadata[m]["genre"])
                    if genre_sync:
                        for genre in (g for g in genres if g not in values):
                            item.removeGenre(genre)
                            logger.info("Detail: Genre {} removed".format(genre))
                    for value in (v for v in values if v not in genres):
                        item.addGenre(value)
                        logger.info("Detail: Genre {} added".format(value))
                else:
                    logger.error("Metadata Error: genre attribute is blank")

            if "label" in self.metadata[m]:
                if self.metadata[m]["label"]:
                    label_sync = False
                    if "label_sync_mode" in self.metadata[m]:
                        if self.metadata[m]["label_sync_mode"] is None:                         logger.error("Metadata Error: label_sync_mode attribute is blank defaulting to append")
                        elif self.metadata[m]["label_sync_mode"] not in ["append", "sync"]:     logger.error("Metadata Error: label_sync_mode attribute must be either 'append' or 'sync' defaulting to append")
                        elif self.metadata[m]["label_sync_mode"] == "sync":                     label_sync = True
                    labels = [label.tag for label in item.labels]
                    values = util.get_list(self.metadata[m]["label"])
                    if label_sync:
                        for label in (l for l in labels if l not in values):
                            item.removeLabel(label)
                            logger.info("Detail: Label {} removed".format(label))
                    for value in (v for v in values if v not in labels):
                        item.addLabel(v)
                        logger.info("Detail: Label {} added".format(v))
                else:
                    logger.error("Metadata Error: label attribute is blank")

            if "seasons" in self.metadata[m] and self.is_show:
                if self.metadata[m]["seasons"]:
                    for season_id in self.metadata[m]["seasons"]:
                        logger.info("")
                        logger.info("Updating season {} of {}...".format(season_id, alt_title if used_alt else m))
                        if isinstance(season_id, int):
                            try:                                season = item.season(season_id)
                            except NotFound:                    logger.error("Metadata Error: Season: {} not found".format(season_id))
                            else:
                                edits = {}
                                add_edit("title", self.metadata[m]["seasons"][season_id], sub=True)
                                add_edit("summary", self.metadata[m]["seasons"][season_id])
                                try:
                                    season.edit(**edits)
                                    season.reload()
                                    logger.info("Season: {} Details Update Successful".format(season_id))
                                except BadRequest:
                                    logger.debug("Season: {} Details Update: {}".format(season_id, edits))
                                    logger.error("Season: {} Details Update Failed".format(season_id))
                                    util.print_stacktrace()
                        else:
                            logger.error("Metadata Error: Season: {} invalid, it must be an integer".format(season_id))
                else:
                    logger.error("Metadata Error: seasons attribute is blank")

            if "episodes" in self.metadata[m] and self.is_show:
                if self.metadata[m]["episodes"]:
                    for episode_str in self.metadata[m]["episodes"]:
                        logger.info("")
                        match = re.search("[Ss]{1}\d+[Ee]{1}\d+", episode_str)
                        if match:
                            output = match.group(0)[1:].split("E" if "E" in m.group(0) else "e")
                            episode_id = int(output[0])
                            season_id = int(output[1])
                            logger.info("Updating episode S{}E{} of {}...".format(episode_id, season_id, alt_title if used_alt else m))
                            try:                                episode = item.episode(season=season_id, episode=episode_id)
                            except NotFound:                    logger.error("Metadata Error: episode {} of season {} not found".format(episode_id, season_id))
                            else:
                                edits = {}
                                add_edit("title", self.metadata[m]["episodes"][episode_str], sub=True)
                                add_edit("sort_title", self.metadata[m]["episodes"][episode_str], key="titleSort")
                                add_edit("rating", self.metadata[m]["episodes"][episode_str])
                                add_edit("originally_available", self.metadata[m]["episodes"][episode_str], key="originallyAvailableAt")
                                add_edit("summary", self.metadata[m]["episodes"][episode_str])
                                try:
                                    episode.edit(**edits)
                                    episode.reload()
                                    logger.info("Season: {} Episode: {} Details Update Successful".format(season_id, episode_id))
                                except BadRequest:
                                    logger.debug("Season: {} Episode: {} Details Update: {}".format(season_id, episode_id, edits))
                                    logger.error("Season: {} Episode: {} Details Update Failed".format(season_id, episode_id))
                                    util.print_stacktrace()
                        else:
                            logger.error("Metadata Error: episode {} invlaid must have S##E## format".format(episode_str))
                else:
                    logger.error("Metadata Error: episodes attribute is blank")
Пример #23
0
    def add_to_collection(self, collection, items, filters, show_filtered, map,
                          movie_map, show_map):
        name = collection.title if isinstance(collection,
                                              Collections) else collection
        collection_items = collection.items() if isinstance(
            collection, Collections) else []
        total = len(items)
        max_length = len(str(total))
        length = 0
        for i, item in enumerate(items, 1):
            try:
                current = self.fetchItem(
                    item.ratingKey if isinstance(item, (Movie,
                                                        Show)) else int(item))
            except (BadRequest, NotFound):
                logger.error("Plex Error: Item {} not found".format(item))
                continue
            match = True
            if filters:
                length = util.print_return(
                    length, "Filtering {}/{} {}".format(
                        (" " * (max_length - len(str(i)))) + str(i), total,
                        current.title))
                for f in filters:
                    modifier = f[0][-4:]
                    method = util.filter_alias[f[0][:-4]] if modifier in [
                        ".not", ".lte", ".gte"
                    ] else util.filter_alias[f[0]]
                    if method == "max_age":
                        threshold_date = datetime.now() - timedelta(days=f[1])
                        attr = getattr(current, "originallyAvailableAt")
                        if attr is None or attr < threshold_date:
                            match = False
                            break
                    elif method == "original_language":
                        terms = util.get_list(f[1], lower=True)
                        tmdb_id = None
                        movie = None
                        for key, value in movie_map.items():
                            if current.ratingKey == value:
                                try:
                                    movie = self.TMDb.get_movie(key)
                                    break
                                except Failed:
                                    pass
                        if movie is None:
                            logger.warning(
                                "Filter Error: No TMDb ID found for {}".format(
                                    current.title))
                            continue
                        if (modifier == ".not"
                                and movie.original_language in terms) or (
                                    modifier != ".not"
                                    and movie.original_language not in terms):
                            match = False
                            break
                    elif modifier in [".gte", ".lte"]:
                        if method == "originallyAvailableAt":
                            threshold_date = datetime.strptime(
                                f[1], "%m/%d/%y")
                            attr = getattr(current, "originallyAvailableAt")
                            if (modifier == ".lte" and attr > threshold_date
                                ) or (modifier == ".gte"
                                      and attr < threshold_date):
                                match = False
                                break
                        elif method in ["year", "rating"]:
                            attr = getattr(current, method)
                            if (modifier == ".lte"
                                    and attr > f[1]) or (modifier == ".gte"
                                                         and attr < f[1]):
                                match = False
                                break
                    else:
                        terms = util.get_list(f[1])
                        if method in [
                                "video_resolution", "audio_language",
                                "subtitle_language"
                        ]:
                            for media in current.media:
                                if method == "video_resolution":
                                    attrs = [media.videoResolution]
                                for part in media.parts:
                                    if method == "audio_language":
                                        attrs = ([
                                            a.language
                                            for a in part.audioStreams()
                                        ])
                                    if method == "subtitle_language":
                                        attrs = ([
                                            s.language
                                            for s in part.subtitleStreams()
                                        ])
                        elif method in [
                                "contentRating", "studio", "year", "rating",
                                "originallyAvailableAt"
                        ]:
                            attrs = [str(getattr(current, method))]
                        elif method in [
                                "actors", "countries", "directors", "genres",
                                "writers", "collections"
                        ]:
                            attrs = [
                                getattr(x, "tag")
                                for x in getattr(current, method)
                            ]

                        if (not list(set(terms) & set(attrs)) and modifier !=
                                ".not") or (list(set(terms) & set(attrs))
                                            and modifier == ".not"):
                            match = False
                            break
                length = util.print_return(
                    length, "Filtering {}/{} {}".format(
                        (" " * (max_length - len(str(i)))) + str(i), total,
                        current.title))
            if match:
                util.print_end(
                    length, "{} Collection | {} | {}".format(
                        name, "=" if current in collection_items else "+",
                        current.title))
                if current in collection_items: map[current.ratingKey] = None
                else: current.addCollection(name)
            elif show_filtered is True:
                logger.info("{} Collection | X | {}".format(
                    name, current.title))
        media_type = "{}{}".format("Movie" if self.is_movie else "Show",
                                   "s" if total > 1 else "")
        util.print_end(length, "{} {} Processed".format(total, media_type))
        return map
Пример #24
0
    def update_libraries(self, test, requested_collections):
        for library in self.libraries:
            os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(library.timeout)
            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 = {c: library.collections[c] for c in util.get_list(requested_collections) if c in library.collections} if requested_collections else 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):
                        no_template_test = True
                        if "template" in collections[c] and collections[c]["template"]:
                            for data_template in util.get_list(collections[c]["template"], split=False):
                                if "name" in data_template \
                                and data_template["name"] \
                                and library.templates \
                                and data_template["name"] in self.library.templates \
                                and self.library.templates[data_template["name"]] \
                                and "test" in self.library.templates[data_template["name"]] \
                                and self.library.templates[data_template["name"]]["test"] == True:
                                    no_template_test = False
                        if no_template_test:
                            continue
                    try:
                        logger.info("")
                        util.seperator("{} Collection".format(c))
                        logger.info("")

                        map = {}
                        try:
                            builder = CollectionBuilder(self, library, c, collections[c])
                        except Exception as e:
                            util.print_stacktrace()
                            logger.error(e)
                            continue

                        try:
                            collection_obj = library.get_collection(c)
                            collection_name = collection_obj.title
                        except Failed as e:
                            collection_obj = None
                            collection_name = c

                        if builder.schedule is not None:
                            print_multiline(builder.schedule, info=True)

                        logger.info("")
                        if builder.sync:
                            logger.info("Sync Mode: sync")
                            if collection_obj:
                                for item in collection_obj.items():
                                    map[item.ratingKey] = item
                        else:
                            logger.info("Sync Mode: append")

                        for i, f in enumerate(builder.filters):
                            if i == 0:
                                logger.info("")
                            logger.info("Collection Filter {}: {}".format(f[0], f[1]))

                        builder.run_methods(collection_obj, collection_name, map, movie_map, show_map)

                        try:
                            plex_collection = library.get_collection(collection_name)
                        except Failed as e:
                            logger.debug(e)
                            continue

                        builder.update_details(plex_collection)

                    except Exception as e:
                        util.print_stacktrace()
                        logger.error("Unknown Error: {}".format(e))
                if library.show_unmanaged is True and not test and not requested_collections:
                    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")
Пример #25
0
    def update_metadata(self, TMDb, test):
        logger.info("")
        util.separator(f"{self.name} Library Metadata")
        logger.info("")
        if not self.metadata:
            raise Failed("No metadata to edit")
        for mapping_name, meta in self.metadata.items():
            methods = {mm.lower(): mm for mm in meta}
            if test and ("test" not in methods or meta[methods["test"]] is not True):
                continue
            logger.info("")
            util.separator()
            logger.info("")
            year = None
            if "year" in methods:
                year = util.check_number(meta[methods["year"]], "year", minimum=1800, maximum=datetime.now().year + 1)

            title = mapping_name
            if "title" in methods:
                if meta[methods["title"]] is None:              logger.error("Metadata Error: title attribute is blank")
                else:                                           title = meta[methods["title"]]

            item = self.search_item(title, year=year)

            if item is None:
                item = self.search_item(f"{title} (SUB)", year=year)

            if item is None and "alt_title" in methods:
                if meta[methods["alt_title"]] is None:
                    logger.error("Metadata Error: alt_title attribute is blank")
                else:
                    alt_title = meta["alt_title"]
                    item = self.search_item(alt_title, year=year)

            if item is None:
                logger.error(f"Plex Error: Item {mapping_name} not found")
                logger.error(f"Skipping {mapping_name}")
                continue

            item_type = "Movie" if self.is_movie else "Show"
            logger.info(f"Updating {item_type}: {title}...")

            tmdb_item = None
            try:
                if "tmdb_id" in methods:
                    if meta[methods["tmdb_id"]] is None:        logger.error("Metadata Error: tmdb_id attribute is blank")
                    elif self.is_show:                          logger.error("Metadata Error: tmdb_id attribute only works with movie libraries")
                    else:                                       tmdb_item = TMDb.get_show(util.regex_first_int(meta[methods["tmdb_id"]], "Show"))
            except Failed as e:
                logger.error(e)

            originally_available = tmdb_item.first_air_date if tmdb_item else None
            rating = tmdb_item.vote_average if tmdb_item else None
            original_title = tmdb_item.original_name if tmdb_item and tmdb_item.original_name != tmdb_item.name else None
            studio = tmdb_item.networks[0].name if tmdb_item else None
            tagline = tmdb_item.tagline if tmdb_item and len(tmdb_item.tagline) > 0 else None
            summary = tmdb_item.overview if tmdb_item else None

            details_updated = False
            advance_details_updated = False
            genre_updated = False
            label_updated = False
            season_updated = False
            episode_updated = False

            edits = {}
            def add_edit(name, current, group, alias, key=None, value=None):
                if value or name in alias:
                    if value or group[alias[name]]:
                        if key is None:         key = name
                        if value is None:       value = group[alias[name]]
                        if str(current) != str(value):
                            edits[f"{key}.value"] = value
                            edits[f"{key}.locked"] = 1
                            logger.info(f"Detail: {name} updated to {value}")
                    else:
                        logger.error(f"Metadata Error: {name} attribute is blank")
            add_edit("title", item.title, meta, methods, value=title)
            add_edit("sort_title", item.titleSort, meta, methods, key="titleSort")
            add_edit("originally_available", str(item.originallyAvailableAt)[:-9], meta, methods, key="originallyAvailableAt", value=originally_available)
            add_edit("rating", item.rating, meta, methods, value=rating)
            add_edit("content_rating", item.contentRating, meta, methods, key="contentRating")
            add_edit("original_title", item.originalTitle, meta, methods, key="originalTitle", value=original_title)
            add_edit("studio", item.studio, meta, methods, value=studio)
            add_edit("tagline", item.tagline, meta, methods, value=tagline)
            add_edit("summary", item.summary, meta, methods, value=summary)
            if len(edits) > 0:
                logger.debug(f"Details Update: {edits}")
                details_updated = True
                try:
                    item.edit(**edits)
                    item.reload()
                    logger.info(f"{item_type}: {mapping_name} Details Update Successful")
                except BadRequest:
                    util.print_stacktrace()
                    logger.error(f"{item_type}: {mapping_name} Details Update Failed")

            advance_edits = {}
            if self.is_show:

                if "episode_sorting" in methods:
                    if meta[methods["episode_sorting"]]:
                        method_data = str(meta[methods["episode_sorting"]]).lower()
                        if method_data in ["default", "oldest", "newest"]:
                            if method_data == "default" and item.episodeSort != "-1":
                                advance_edits["episodeSort"] = "-1"
                            elif method_data == "oldest" and item.episodeSort != "0":
                                advance_edits["episodeSort"] = "0"
                            elif method_data == "newest" and item.episodeSort != "1":
                                advance_edits["episodeSort"] = "1"
                            if "episodeSort" in advance_edits:
                                logger.info(f"Detail: episode_sorting updated to {method_data}")
                        else:
                            logger.error(f"Metadata Error: {meta[methods['episode_sorting']]} episode_sorting attribute invalid")
                    else:
                        logger.error(f"Metadata Error: episode_sorting attribute is blank")

                if "keep_episodes" in methods:
                    if meta[methods["keep_episodes"]]:
                        method_data = str(meta[methods["keep_episodes"]]).lower()
                        if method_data in ["all", "5_latest", "3_latest", "latest", "past_3", "past_7", "past_30"]:
                            if method_data == "all" and item.autoDeletionItemPolicyUnwatchedLibrary != 0:
                                advance_edits["autoDeletionItemPolicyUnwatchedLibrary"] = 0
                            elif method_data == "5_latest" and item.autoDeletionItemPolicyUnwatchedLibrary != 5:
                                advance_edits["autoDeletionItemPolicyUnwatchedLibrary"] = 5
                            elif method_data == "3_latest" and item.autoDeletionItemPolicyUnwatchedLibrary != 3:
                                advance_edits["autoDeletionItemPolicyUnwatchedLibrary"] = 3
                            elif method_data == "latest" and item.autoDeletionItemPolicyUnwatchedLibrary != 1:
                                advance_edits["autoDeletionItemPolicyUnwatchedLibrary"] = 1
                            elif method_data == "past_3" and item.autoDeletionItemPolicyUnwatchedLibrary != -3:
                                advance_edits["autoDeletionItemPolicyUnwatchedLibrary"] = -3
                            elif method_data == "past_7" and item.autoDeletionItemPolicyUnwatchedLibrary != -7:
                                advance_edits["autoDeletionItemPolicyUnwatchedLibrary"] = -7
                            elif method_data == "past_30" and item.autoDeletionItemPolicyUnwatchedLibrary != -30:
                                advance_edits["autoDeletionItemPolicyUnwatchedLibrary"] = -30
                            if "autoDeletionItemPolicyUnwatchedLibrary" in advance_edits:
                                logger.info(f"Detail: keep_episodes updated to {method_data}")
                        else:
                            logger.error(f"Metadata Error: {meta[methods['keep_episodes']]} keep_episodes attribute invalid")
                    else:
                        logger.error(f"Metadata Error: keep_episodes attribute is blank")

                if "delete_episodes" in methods:
                    if meta[methods["delete_episodes"]]:
                        method_data = str(meta[methods["delete_episodes"]]).lower()
                        if method_data in ["never", "day", "week", "refresh"]:
                            if method_data == "never" and item.autoDeletionItemPolicyWatchedLibrary != 0:
                                advance_edits["autoDeletionItemPolicyWatchedLibrary"] = 0
                            elif method_data == "day" and item.autoDeletionItemPolicyWatchedLibrary != 1:
                                advance_edits["autoDeletionItemPolicyWatchedLibrary"] = 1
                            elif method_data == "week" and item.autoDeletionItemPolicyWatchedLibrary != 7:
                                advance_edits["autoDeletionItemPolicyWatchedLibrary"] = 7
                            elif method_data == "refresh" and item.autoDeletionItemPolicyWatchedLibrary != 100:
                                advance_edits["autoDeletionItemPolicyWatchedLibrary"] = 100
                            if "autoDeletionItemPolicyWatchedLibrary" in advance_edits:
                                logger.info(f"Detail: delete_episodes updated to {method_data}")
                        else:
                            logger.error(f"Metadata Error: {meta[methods['delete_episodes']]} delete_episodes attribute invalid")
                    else:
                        logger.error(f"Metadata Error: delete_episodes attribute is blank")

                if "season_display" in methods:
                    if meta[methods["season_display"]]:
                        method_data = str(meta[methods["season_display"]]).lower()
                        if method_data in ["default", "hide", "show"]:
                            if method_data == "default" and item.flattenSeasons != -1:
                                advance_edits["flattenSeasons"] = -1
                            elif method_data == "show" and item.flattenSeasons != 0:
                                advance_edits["flattenSeasons"] = 0
                            elif method_data == "hide" and item.flattenSeasons != 1:
                                advance_edits["flattenSeasons"] = 1
                            if "flattenSeasons" in advance_edits:
                                logger.info(f"Detail: season_display updated to {method_data}")
                        else:
                            logger.error(f"Metadata Error: {meta[methods['season_display']]} season_display attribute invalid")
                    else:
                        logger.error(f"Metadata Error: season_display attribute is blank")

                if "episode_ordering" in methods:
                    if meta[methods["episode_ordering"]]:
                        method_data = str(meta[methods["episode_ordering"]]).lower()
                        if method_data in ["default", "tmdb_aired", "tvdb_aired", "tvdb_dvd", "tvdb_absolute"]:
                            if method_data == "default" and item.showOrdering is not None:
                                advance_edits["showOrdering"] = None
                            elif method_data == "tmdb_aired" and item.showOrdering != "tmdbAiring":
                                advance_edits["showOrdering"] = "tmdbAiring"
                            elif method_data == "tvdb_aired" and item.showOrdering != "airing":
                                advance_edits["showOrdering"] = "airing"
                            elif method_data == "tvdb_dvd" and item.showOrdering != "dvd":
                                advance_edits["showOrdering"] = "dvd"
                            elif method_data == "tvdb_absolute" and item.showOrdering != "absolute":
                                advance_edits["showOrdering"] = "absolute"
                            if "showOrdering" in advance_edits:
                                logger.info(f"Detail: episode_ordering updated to {method_data}")
                        else:
                            logger.error(f"Metadata Error: {meta[methods['episode_ordering']]} episode_ordering attribute invalid")
                    else:
                        logger.error(f"Metadata Error: episode_ordering attribute is blank")

            if "metadata_language" in methods:
                if meta[methods["metadata_language"]]:
                    method_data = str(meta[methods["metadata_language"]]).lower()
                    lower_languages = {la.lower(): la for la in util.plex_languages}
                    if method_data in lower_languages:
                        if method_data == "default" and item.languageOverride is None:
                            advance_edits["languageOverride"] = None
                        elif str(item.languageOverride).lower() != lower_languages[method_data]:
                            advance_edits["languageOverride"] = lower_languages[method_data]
                        if "languageOverride" in advance_edits:
                            logger.info(f"Detail: metadata_language updated to {method_data}")
                    else:
                        logger.error(f"Metadata Error: {meta[methods['metadata_language']]} metadata_language attribute invalid")
                else:
                    logger.error(f"Metadata Error: metadata_language attribute is blank")

            if "use_original_title" in methods:
                if meta[methods["use_original_title"]]:
                    method_data = str(meta[methods["use_original_title"]]).lower()
                    if method_data in ["default", "no", "yes"]:
                        if method_data == "default" and item.useOriginalTitle != -1:
                            advance_edits["useOriginalTitle"] = -1
                        elif method_data == "no" and item.useOriginalTitle != 0:
                            advance_edits["useOriginalTitle"] = 0
                        elif method_data == "yes" and item.useOriginalTitle != 1:
                            advance_edits["useOriginalTitle"] = 1
                        if "useOriginalTitle" in advance_edits:
                            logger.info(f"Detail: use_original_title updated to {method_data}")
                    else:
                        logger.error(f"Metadata Error: {meta[methods['use_original_title']]} use_original_title attribute invalid")
                else:
                    logger.error(f"Metadata Error: use_original_title attribute is blank")

            if len(advance_edits) > 0:
                logger.debug(f"Details Update: {advance_edits}")
                advance_details_updated = True
                try:
                    check_dict = {pref.id: list(pref.enumValues.keys()) for pref in item.preferences()}
                    logger.info(check_dict)
                    item.editAdvanced(**advance_edits)
                    item.reload()
                    logger.info(f"{item_type}: {mapping_name} Advanced Details Update Successful")
                except BadRequest:
                    util.print_stacktrace()
                    logger.error(f"{item_type}: {mapping_name} Advanced Details Update Failed")

            genres = []
            if tmdb_item:
                genres.extend([genre.name for genre in tmdb_item.genres])
            if "genre" in methods:
                if meta[methods["genre"]]:
                    genres.extend(util.get_list(meta[methods["genre"]]))
                else:
                    logger.error("Metadata Error: genre attribute is blank")
            if len(genres) > 0:
                item_genres = [genre.tag for genre in item.genres]
                if "genre_sync_mode" in methods:
                    if meta[methods["genre_sync_mode"]] is None:
                        logger.error("Metadata Error: genre_sync_mode attribute is blank defaulting to append")
                    elif str(meta[methods["genre_sync_mode"]]).lower() not in ["append", "sync"]:
                        logger.error("Metadata Error: genre_sync_mode attribute must be either 'append' or 'sync' defaulting to append")
                    elif str(meta["genre_sync_mode"]).lower() == "sync":
                        for genre in (g for g in item_genres if g not in genres):
                            genre_updated = True
                            item.removeGenre(genre)
                            logger.info(f"Detail: Genre {genre} removed")
                for genre in (g for g in genres if g not in item_genres):
                    genre_updated = True
                    item.addGenre(genre)
                    logger.info(f"Detail: Genre {genre} added")

            if "label" in methods:
                if meta[methods["label"]]:
                    item_labels = [label.tag for label in item.labels]
                    labels = util.get_list(meta[methods["label"]])
                    if "label_sync_mode" in methods:
                        if meta[methods["label_sync_mode"]] is None:
                            logger.error("Metadata Error: label_sync_mode attribute is blank defaulting to append")
                        elif str(meta[methods["label_sync_mode"]]).lower() not in ["append", "sync"]:
                            logger.error("Metadata Error: label_sync_mode attribute must be either 'append' or 'sync' defaulting to append")
                        elif str(meta[methods["label_sync_mode"]]).lower() == "sync":
                            for label in (la for la in item_labels if la not in labels):
                                label_updated = True
                                item.removeLabel(label)
                                logger.info(f"Detail: Label {label} removed")
                    for label in (la for la in labels if la not in item_labels):
                        label_updated = True
                        item.addLabel(label)
                        logger.info(f"Detail: Label {label} added")
                else:
                    logger.error("Metadata Error: label attribute is blank")

            if "seasons" in methods and self.is_show:
                if meta[methods["seasons"]]:
                    for season_id in meta[methods["seasons"]]:
                        logger.info("")
                        logger.info(f"Updating season {season_id} of {mapping_name}...")
                        if isinstance(season_id, int):
                            try:                                season = item.season(season_id)
                            except NotFound:                    logger.error(f"Metadata Error: Season: {season_id} not found")
                            else:
                                season_dict = meta[methods["seasons"]][season_id]
                                season_methods = {sm.lower(): sm for sm in season_dict}

                                if "title" in season_methods and season_dict[season_methods["title"]]:
                                    title = season_dict[season_methods["title"]]
                                else:
                                    title = season.title
                                if "sub" in season_methods:
                                    if season_dict[season_methods["sub"]] is None:
                                        logger.error("Metadata Error: sub attribute is blank")
                                    elif season_dict[season_methods["sub"]] is True and "(SUB)" not in title:
                                        title = f"{title} (SUB)"
                                    elif season_dict[season_methods["sub"]] is False and title.endswith(" (SUB)"):
                                        title = title[:-6]
                                    else:
                                        logger.error("Metadata Error: sub attribute must be True or False")

                                edits = {}
                                add_edit("title", season.title, season_dict, season_methods, value=title)
                                add_edit("summary", season.summary, season_methods, season_dict)
                                if len(edits) > 0:
                                    logger.debug(f"Season: {season_id} Details Update: {edits}")
                                    season_updated = True
                                    try:
                                        season.edit(**edits)
                                        season.reload()
                                        logger.info(f"Season: {season_id} Details Update Successful")
                                    except BadRequest:
                                        util.print_stacktrace()
                                        logger.error(f"Season: {season_id} Details Update Failed")
                        else:
                            logger.error(f"Metadata Error: Season: {season_id} invalid, it must be an integer")
                else:
                    logger.error("Metadata Error: seasons attribute is blank")

            if "episodes" in methods and self.is_show:
                if meta[methods["episodes"]]:
                    for episode_str in meta[methods["episodes"]]:
                        logger.info("")
                        match = re.search("[Ss]\\d+[Ee]\\d+", episode_str)
                        if match:
                            output = match.group(0)[1:].split("E" if "E" in match.group(0) else "e")
                            episode_id = int(output[0])
                            season_id = int(output[1])
                            logger.info(f"Updating episode S{episode_id}E{season_id} of {mapping_name}...")
                            try:                                episode = item.episode(season=season_id, episode=episode_id)
                            except NotFound:                    logger.error(f"Metadata Error: episode {episode_id} of season {season_id} not found")
                            else:
                                episode_dict = meta[methods["episodes"]][episode_str]
                                episode_methods = {em.lower(): em for em in episode_dict}

                                if "title" in episode_methods and episode_dict[episode_methods["title"]]:
                                    title = episode_dict[episode_methods["title"]]
                                else:
                                    title = episode.title
                                if "sub" in episode_dict:
                                    if episode_dict[episode_methods["sub"]] is None:
                                        logger.error("Metadata Error: sub attribute is blank")
                                    elif episode_dict[episode_methods["sub"]] is True and "(SUB)" not in title:
                                        title = f"{title} (SUB)"
                                    elif episode_dict[episode_methods["sub"]] is False and title.endswith(" (SUB)"):
                                        title = title[:-6]
                                    else:
                                        logger.error("Metadata Error: sub attribute must be True or False")
                                edits = {}
                                add_edit("title", episode.title, episode_dict, episode_methods, value=title)
                                add_edit("sort_title", episode.titleSort, episode_dict, episode_methods, key="titleSort")
                                add_edit("rating", episode.rating, episode_dict, episode_methods)
                                add_edit("originally_available", str(episode.originallyAvailableAt)[:-9], episode_dict, episode_methods, key="originallyAvailableAt")
                                add_edit("summary", episode.summary, episode_dict, episode_methods)
                                if len(edits) > 0:
                                    logger.debug(f"Season: {season_id} Episode: {episode_id} Details Update: {edits}")
                                    episode_updated = True
                                    try:
                                        episode.edit(**edits)
                                        episode.reload()
                                        logger.info(
                                            f"Season: {season_id} Episode: {episode_id} Details Update Successful")
                                    except BadRequest:
                                        util.print_stacktrace()
                                        logger.error(f"Season: {season_id} Episode: {episode_id} Details Update Failed")
                        else:
                            logger.error(f"Metadata Error: episode {episode_str} invalid must have S##E## format")
                else:
                    logger.error("Metadata Error: episodes attribute is blank")

            if not details_updated and not advance_details_updated and not genre_updated and not label_updated and not season_updated and not episode_updated:
                logger.info(f"{item_type}: {mapping_name} Details Update Not Needed")
def run_collection(config, library, metadata, requested_collections):
    global stats
    logger.info("")
    for mapping_name, collection_attrs in requested_collections.items():
        collection_start = datetime.now()
        if config.test_mode 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

        if config.resume_from and config.resume_from != mapping_name:
            continue
        elif config.resume_from == mapping_name:
            config.resume_from = None
            logger.info("")
            util.separator(f"Resuming Collections")

        if "name_mapping" in collection_attrs and collection_attrs["name_mapping"]:
            collection_log_name, output_str = util.validate_filename(collection_attrs["name_mapping"])
        else:
            collection_log_name, output_str = util.validate_filename(mapping_name)
        collection_log_folder = os.path.join(default_dir, "logs", library.mapping_name, "collections", collection_log_name)
        os.makedirs(collection_log_folder, exist_ok=True)
        col_file_logger = os.path.join(collection_log_folder, "collection.log")
        should_roll_over = os.path.isfile(col_file_logger)
        collection_handler = RotatingFileHandler(col_file_logger, delay=True, mode="w", backupCount=3, encoding="utf-8")
        util.apply_formatter(collection_handler)
        if should_roll_over:
            collection_handler.doRollover()
        logger.addHandler(collection_handler)

        try:
            util.separator(f"{mapping_name} Collection")
            logger.info("")
            if output_str:
                logger.info(output_str)
                logger.info("")

            util.separator(f"Validating {mapping_name} Attributes", space=False, border=False)

            builder = CollectionBuilder(config, library, metadata, mapping_name, no_missing, collection_attrs)
            logger.info("")

            util.separator(f"Running {mapping_name} Collection", space=False, border=False)

            if len(builder.schedule) > 0:
                util.print_multiline(builder.schedule, info=True)

            if len(builder.smart_filter_details) > 0:
                logger.info("")
                util.print_multiline(builder.smart_filter_details, info=True)

            items_added = 0
            items_removed = 0
            if not builder.smart_url and builder.builders:
                logger.info("")
                logger.info(f"Sync Mode: {'sync' if builder.sync else 'append'}")

                if builder.filters or builder.tmdb_filters:
                    logger.info("")
                    for filter_key, filter_value in builder.filters:
                        logger.info(f"Collection Filter {filter_key}: {filter_value}")
                    for filter_key, filter_value in builder.tmdb_filters:
                        logger.info(f"Collection Filter {filter_key}: {filter_value}")

                builder.find_rating_keys()

                if len(builder.rating_keys) >= builder.minimum and builder.build_collection:
                    logger.info("")
                    util.separator(f"Adding to {mapping_name} Collection", space=False, border=False)
                    logger.info("")
                    items_added = builder.add_to_collection()
                    stats["added"] += items_added
                    items_removed = 0
                    if builder.sync:
                        items_removed = builder.sync_collection()
                        stats["removed"] += items_removed
                elif len(builder.rating_keys) < builder.minimum and builder.build_collection:
                    logger.info("")
                    logger.info(f"Collection Minimum: {builder.minimum} not met for {mapping_name} Collection")
                    if builder.details["delete_below_minimum"] and builder.obj:
                        builder.delete_collection()
                        builder.deleted = True
                        logger.info("")
                        logger.info(f"Collection {builder.obj.title} deleted")

                if builder.do_missing and (len(builder.missing_movies) > 0 or len(builder.missing_shows) > 0):
                    if builder.details["show_missing"] is True:
                        logger.info("")
                        util.separator(f"Missing from Library", space=False, border=False)
                        logger.info("")
                    radarr_add, sonarr_add = builder.run_missing()
                    stats["radarr"] += radarr_add
                    stats["sonarr"] += sonarr_add

            run_item_details = True
            if builder.build_collection and builder.builders:
                try:
                    builder.load_collection()
                    if builder.created:
                        stats["created"] += 1
                    elif items_added > 0 or items_removed > 0:
                        stats["modified"] += 1
                except Failed:
                    util.print_stacktrace()
                    run_item_details = False
                    logger.info("")
                    util.separator("No Collection to Update", space=False, border=False)
                else:
                    builder.update_details()
                    if builder.custom_sort:
                        library.run_sort.append(builder)
                        # builder.sort_collection()

            if builder.deleted:
                stats["deleted"] += 1

            if builder.server_preroll is not None:
                library.set_server_preroll(builder.server_preroll)
                logger.info("")
                logger.info(f"Plex Server Movie pre-roll video updated to {builder.server_preroll}")

            builder.send_notifications()

            if builder.item_details and run_item_details and builder.builders:
                try:
                    builder.load_collection_items()
                except Failed:
                    logger.info("")
                    util.separator("No Items Found", space=False, border=False)
                else:
                    builder.update_item_details()

            if builder.run_again and (len(builder.run_again_movies) > 0 or len(builder.run_again_shows) > 0):
                library.run_again.append(builder)


        except NotScheduled as e:
            util.print_multiline(e, info=True)
        except Failed as e:
            library.notify(e, collection=mapping_name)
            util.print_stacktrace()
            util.print_multiline(e, error=True)
        except Exception as e:
            library.notify(f"Unknown Error: {e}", collection=mapping_name)
            util.print_stacktrace()
            logger.error(f"Unknown Error: {e}")
        logger.info("")
        util.separator(f"Finished {mapping_name} Collection\nCollection Run Time: {str(datetime.now() - collection_start).split('.')[0]}")
        logger.removeHandler(collection_handler)