diff --git a/README.md b/README.md index c3e58e0eb..99915075c 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Plex Meta Manager -#### Version 1.6.2 +#### Version 1.6.4 The original concept for Plex Meta Manager is [Plex Auto Collections](https://github.com/mza921/Plex-Auto-Collections), but this is rewritten from the ground up to be able to include a scheduler, metadata edits, multiple libraries, and logging. Plex Meta Manager is a Python 3 script that can be continuously run using YAML configuration files to update on a schedule the metadata of the movies, shows, and collections in your libraries as well as automatically build collections based on various methods all detailed in the wiki. Some collection examples that the script can automatically build and update daily include Plex Based Searches like actor, genre, or studio collections or Collections based on TMDb, IMDb, Trakt, TVDb, AniDB, or MyAnimeList lists and various other services. diff --git a/modules/builder.py b/modules/builder.py index c7eda519b..4ced414eb 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -248,7 +248,7 @@ def replace_txt(txt): elif method_name == "tvdb_description": self.summaries[method_name] = config.TVDb.get_list_description(method_data, self.library.Plex.language) elif method_name == "trakt_description": - self.summaries[method_name] = config.Trakt.standard_list(config.Trakt.validate_trakt_list(util.get_list(method_data))[0]).description + self.summaries[method_name] = config.Trakt.standard_list(config.Trakt.validate_trakt(util.get_list(method_data))[0]).description elif method_name == "letterboxd_description": self.summaries[method_name] = config.Letterboxd.get_list_description(method_data, self.library.Plex.language) elif method_name == "collection_mode": @@ -266,7 +266,7 @@ def replace_txt(txt): if str(method_data).lower() == "release": self.details[method_name] = "release" elif str(method_data).lower() == "alpha": - self.details[method_name] = "release" + self.details[method_name] = "alpha" else: raise Failed(f"Collection Error: {method_data} collection_order invalid\n\trelease (Order Collection by release dates)\n\talpha (Order Collection Alphabetically)") elif method_name == "url_poster": @@ -309,11 +309,11 @@ def replace_txt(txt): elif method_name in ["title", "title.and", "title.not", "title.begins", "title.ends"]: self.methods.append(("plex_search", [{method_name: util.get_list(method_data, split=False)}])) elif method_name in ["decade", "year.greater", "year.less"]: - self.methods.append(("plex_search", [{method_name: util.check_year(method_data, current_year, method_name)}])) + self.methods.append(("plex_search", [{method_name: [util.check_year(method_data, current_year, method_name)]}])) elif method_name in ["added.before", "added.after", "originally_available.before", "originally_available.after"]: - self.methods.append(("plex_search", [{method_name: util.check_date(method_data, method_name, return_string=True, plex_date=True)}])) + self.methods.append(("plex_search", [{method_name: [util.check_date(method_data, method_name, return_string=True, plex_date=True)]}])) elif method_name in ["duration.greater", "duration.less", "rating.greater", "rating.less"]: - self.methods.append(("plex_search", [{method_name: util.check_number(method_data, method_name, minimum=0)}])) + self.methods.append(("plex_search", [{method_name: [util.check_number(method_data, method_name, minimum=0)]}])) elif method_name in ["year", "year.not"]: self.methods.append(("plex_search", [{method_name: util.get_year_list(method_data, current_year, method_name)}])) elif method_name in util.tmdb_searches: @@ -355,15 +355,15 @@ def replace_txt(txt): elif method_name in ["anilist_id", "anilist_relations", "anilist_studio"]: self.methods.append((method_name, config.AniList.validate_anilist_ids(util.get_int_list(method_data, "AniList ID"), studio=method_name == "anilist_studio"))) elif method_name == "trakt_list": - self.methods.append((method_name, config.Trakt.validate_trakt_list(util.get_list(method_data)))) + self.methods.append((method_name, config.Trakt.validate_trakt(util.get_list(method_data)))) elif method_name == "trakt_list_details": - valid_list = config.Trakt.validate_trakt_list(util.get_list(method_data)) + valid_list = config.Trakt.validate_trakt(util.get_list(method_data)) item = config.Trakt.standard_list(valid_list[0]) if hasattr(item, "description") and item.description: self.summaries[method_name] = item.description self.methods.append((method_name[:-8], valid_list)) - elif method_name == "trakt_watchlist": - self.methods.append((method_name, config.Trakt.validate_trakt_watchlist(util.get_list(method_data), self.library.is_movie))) + elif method_name in ["trakt_watchlist", "trakt_collection"]: + self.methods.append((method_name, config.Trakt.validate_trakt(method_name[6:], util.get_list(method_data), self.library.is_movie))) elif method_name == "imdb_list": new_list = [] for imdb_list in util.get_list(method_data, split=False): @@ -492,11 +492,11 @@ def get_int(parent, method, data_in, methods_in, default_in, minimum=1, maximum= searches[search_final] = self.library.validate_search_list(final_values, search) elif (search == "decade" and modifier in [""]) \ or (search == "year" and modifier in [".greater", ".less"]): - searches[search_final] = util.check_year(search_data, current_year, search_final) + searches[search_final] = [util.check_year(search_data, current_year, search_final)] elif search in ["added", "originally_available"] and modifier in [".before", ".after"]: - searches[search_final] = util.check_date(search_data, search_final, return_string=True, plex_date=True) + searches[search_final] = [util.check_date(search_data, search_final, return_string=True, plex_date=True)] elif search in ["duration", "rating"] and modifier in [".greater", ".less"]: - searches[search_final] = util.check_number(search_data, search_final, minimum=0) + searches[search_final] = [util.check_number(search_data, search_final, minimum=0)] elif search == "year" and modifier in ["", ".not"]: searches[search_final] = util.get_year_list(search_data, current_year, search_final) elif (search in ["title", "studio"] and modifier not in ["", ".and", ".not", ".begins", ".ends"]) \ diff --git a/modules/config.py b/modules/config.py index 8c4b5b910..7ceae2211 100644 --- a/modules/config.py +++ b/modules/config.py @@ -99,7 +99,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" elif attribute not in loaded_config[parent]: loaded_config[parent][attribute] = default else: endline = "" yaml.round_trip_dump(loaded_config, open(self.config_path, "w"), indent=ind_in, block_seq_indent=bsi_in) - elif not data[attribute] and data[attribute] is not False: + elif data[attribute] is None: if default_is_none is True: return None else: message = f"{text} is blank" elif var_type == "bool": @@ -325,7 +325,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" library = PlexAPI(params, self.TMDb, self.TVDb) logger.info(f"{params['name']} Library Connection Successful") except Failed as e: - logger.error(e) + util.print_multiline(e, error=True) logger.info(f"{params['name']} Library Connection Failed") continue @@ -343,7 +343,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" radarr_params["tag"] = check_for_attribute(lib, "search", parent="radarr", var_type="lower_list", default=self.general["radarr"]["tag"], default_is_none=True, save=False) library.Radarr = RadarrAPI(self.TMDb, radarr_params) except Failed as e: - util.print_multiline(e) + util.print_multiline(e, error=True) logger.info(f"{params['name']} library's Radarr Connection {'Failed' if library.Radarr is None else 'Successful'}") if self.general["sonarr"]["url"] or "sonarr" in lib: @@ -361,7 +361,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" sonarr_params["tag"] = check_for_attribute(lib, "search", parent="sonarr", var_type="lower_list", default=self.general["sonarr"]["tag"], default_is_none=True, save=False) library.Sonarr = SonarrAPI(self.TVDb, sonarr_params, library.Plex.language) except Failed as e: - util.print_multiline(e) + util.print_multiline(e, error=True) logger.info(f"{params['name']} library's Sonarr Connection {'Failed' if library.Sonarr is None else 'Successful'}") if self.general["tautulli"]["url"] or "tautulli" in lib: @@ -372,7 +372,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" tautulli_params["apikey"] = check_for_attribute(lib, "apikey", parent="tautulli", default=self.general["tautulli"]["apikey"], req_default=True, save=False) library.Tautulli = TautulliAPI(tautulli_params) except Failed as e: - util.print_multiline(e) + util.print_multiline(e, error=True) logger.info(f"{params['name']} library's Tautulli Connection {'Failed' if library.Tautulli is None else 'Successful'}") self.libraries.append(library) diff --git a/modules/plex.py b/modules/plex.py index c18944a76..bc482813a 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -306,6 +306,13 @@ def update_metadata(self, TMDb, test): 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: @@ -329,6 +336,7 @@ def add_edit(name, current, group, alias, key=None, value=None): 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() @@ -336,8 +344,6 @@ def add_edit(name, current, group, alias, key=None, value=None): except BadRequest: util.print_stacktrace() logger.error(f"{item_type}: {mapping_name} Details Update Failed") - else: - logger.info(f"{item_type}: {mapping_name} Details Update Not Needed") advance_edits = {} if self.is_show: @@ -476,6 +482,7 @@ def add_edit(name, current, group, alias, key=None, value=None): 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) @@ -484,9 +491,7 @@ def add_edit(name, current, group, alias, key=None, value=None): logger.info(f"{item_type}: {mapping_name} Advanced Details Update Successful") except BadRequest: util.print_stacktrace() - logger.error(f"{item_type}: {mapping_name} Details Update Failed") - else: - logger.info(f"{item_type}: {mapping_name} Details Update Not Needed") + logger.error(f"{item_type}: {mapping_name} Advanced Details Update Failed") genres = [] if tmdb_item: @@ -505,9 +510,11 @@ def add_edit(name, current, group, alias, key=None, value=None): 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") @@ -522,9 +529,11 @@ def add_edit(name, current, group, alias, key=None, value=None): 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: @@ -561,6 +570,7 @@ def add_edit(name, current, group, alias, key=None, value=None): 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() @@ -568,8 +578,6 @@ def add_edit(name, current, group, alias, key=None, value=None): 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: @@ -612,6 +620,7 @@ def add_edit(name, current, group, alias, key=None, value=None): 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() @@ -620,9 +629,10 @@ def add_edit(name, current, group, alias, key=None, value=None): 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") + + 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") \ No newline at end of file diff --git a/modules/trakttv.py b/modules/trakttv.py index 1c41c6226..1f48479d2 100644 --- a/modules/trakttv.py +++ b/modules/trakttv.py @@ -111,6 +111,11 @@ def standard_list(self, data): def send_request(self, url): return requests.get(url, headers={"Content-Type": "application/json", "trakt-api-version": "2", "trakt-api-key": self.client_id}).json() + def get_collection(self, username, is_movie): + items = self.send_request(f"{self.base_url}/users/{username}/collection/{'movies' if is_movie else 'shows'}") + if is_movie: return [item["movie"]["ids"]["tmdb"] for item in items], [] + else: return [], [item["show"]["ids"]["tvdb"] for item in items] + def get_pagenation(self, pagenation, amount, is_movie): items = self.send_request(f"{self.base_url}/{'movies' if is_movie else 'shows'}/{pagenation}?limit={amount}") if pagenation == "popular" and is_movie: return [item["ids"]["tmdb"] for item in items], [] @@ -118,28 +123,26 @@ def get_pagenation(self, pagenation, amount, is_movie): elif is_movie: return [item["movie"]["ids"]["tmdb"] for item in items], [] else: return [], [item["show"]["ids"]["tvdb"] for item in items] - def validate_trakt_list(self, values): - trakt_values = [] - for value in values: - try: - self.standard_list(value) - trakt_values.append(value) - except Failed as e: - logger.error(e) - if len(trakt_values) == 0: - raise Failed(f"Trakt Error: No valid Trakt Lists in {values}") - return trakt_values - - def validate_trakt_watchlist(self, values, is_movie): + def validate_trakt(self, values, trakt_type=None, is_movie=None): trakt_values = [] for value in values: try: - self.watchlist(value, is_movie) + if trakt_type == "watchlist" and is_movie is not None: + self.watchlist(value, is_movie) + elif trakt_type == "collection" and is_movie is not None: + self.get_collection(value, is_movie) + else: + self.standard_list(value) trakt_values.append(value) except Failed as e: logger.error(e) if len(trakt_values) == 0: - raise Failed(f"Trakt Error: No valid Trakt Watchlists in {values}") + if trakt_type == "watchlist" and is_movie is not None: + raise Failed(f"Trakt Error: No valid Trakt Watchlists in {values}") + elif trakt_type == "collection" and is_movie is not None: + 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 get_items(self, method, data, is_movie, status_message=True): @@ -151,6 +154,10 @@ def get_items(self, method, data, is_movie, status_message=True): movie_ids, show_ids = self.get_pagenation(method[6:], data, is_movie) if status_message: logger.info(f"Processing {pretty}: {data} {media_type}{'' if data == 1 else 's'}") + elif method == "trakt_collection": + movie_ids, show_ids = self.get_collection(data, is_movie) + if status_message: + logger.info(f"Processing {pretty} {media_type}s for {data}") else: show_ids = [] movie_ids = [] diff --git a/modules/util.py b/modules/util.py index 99e45c4c5..4c032273d 100644 --- a/modules/util.py +++ b/modules/util.py @@ -155,6 +155,7 @@ def retry_if_not_failed(exception): "tmdb_writer": "TMDb Writer", "tmdb_writer_details": "TMDb Writer", "trakt_collected": "Trakt Collected", + "trakt_collection": "Trakt Collection", "trakt_list": "Trakt List", "trakt_list_details": "Trakt List", "trakt_popular": "Trakt Popular", @@ -296,6 +297,7 @@ def retry_if_not_failed(exception): "tmdb_writer", "tmdb_writer_details", "trakt_collected", + "trakt_collection", "trakt_list", "trakt_list_details", "trakt_popular", diff --git a/plex_meta_manager.py b/plex_meta_manager.py index df700524e..91f84025f 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -72,7 +72,7 @@ def fmt_filter(record): cmd_handler = logging.StreamHandler() cmd_handler.setFormatter(logging.Formatter("| %(message)-100s |")) -cmd_handler.setLevel(logging.DEBUG if tests or test or debug else logging.INFO) +cmd_handler.setLevel(logging.DEBUG if my_tests or test or debug else logging.INFO) logger.addHandler(cmd_handler) logger.addHandler(file_handler) @@ -87,23 +87,23 @@ def fmt_filter(record): util.centered("| __/| | __/> < | | | | __/ || (_| | | | | | (_| | | | | (_| | (_| | __/ | ") util.centered("|_| |_|\\___/_/\\_\\ |_| |_|\\___|\\__\\__,_| |_| |_|\\__,_|_| |_|\\__,_|\\__, |\\___|_| ") util.centered(" |___/ ") -util.centered(" Version: 1.6.2 ") +util.centered(" Version: 1.6.4 ") util.separator() if my_tests: tests.run_tests(default_dir) sys.exit(0) -def start(config_path, is_test, daily, collections): +def start(config_path, is_test, daily, collections_to_run): if daily: start_type = "Daily " elif is_test: start_type = "Test " - elif collections: start_type = "Collections " + elif collections_to_run: start_type = "Collections " else: start_type = "" start_time = datetime.now() util.separator(f"Starting {start_type}Run") try: config = Config(default_dir, config_path) - config.update_libraries(is_test, collections) + config.update_libraries(is_test, collections_to_run) except Exception as e: util.print_stacktrace() logger.critical(e)