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
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
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")
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"]
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
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
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
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
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
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()])}" )
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
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
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
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"])
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
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
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
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
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")
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")
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")
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
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")
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)