diff --git a/modules/builder.py b/modules/builder.py index 96cbe2b5..49385abc 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -76,7 +76,10 @@ summary_details = [ ] poster_details = ["url_poster", "tmdb_poster", "tmdb_profile", "tvdb_poster", "file_poster"] background_details = ["url_background", "tmdb_background", "tvdb_background", "file_background"] -boolean_details = ["visible_library", "visible_home", "visible_shared", "show_filtered", "show_missing", "save_missing", "missing_only_released", "delete_below_minimum"] +boolean_details = [ + "visible_library", "visible_home", "visible_shared", "show_filtered", "show_missing", "save_missing", "missing_only_released", + "delete_below_minimum", "notifiarr_collection_creation", "notifiarr_collection_addition", "notifiarr_collection_removing" +] string_details = ["sort_title", "content_rating", "name_mapping"] ignored_details = [ "smart_filter", "smart_label", "smart_url", "run_again", "schedule", "sync_mode", "template", "test", @@ -168,7 +171,10 @@ class CollectionBuilder: "save_missing": self.library.save_missing, "missing_only_released": self.library.missing_only_released, "create_asset_folders": self.library.create_asset_folders, - "delete_below_minimum": self.library.delete_below_minimum + "delete_below_minimum": self.library.delete_below_minimum, + "notifiarr_collection_creation": self.library.notifiarr_collection_creation, + "notifiarr_collection_addition": self.library.notifiarr_collection_addition, + "notifiarr_collection_removing": self.library.notifiarr_collection_removing, } self.item_details = {} self.radarr_details = {} @@ -183,6 +189,8 @@ class CollectionBuilder: self.filtered_keys = {} self.run_again_movies = [] self.run_again_shows = [] + self.notifiarr_additions = [] + self.notifiarr_removals = [] self.items = [] self.posters = {} self.backgrounds = {} @@ -191,6 +199,8 @@ class CollectionBuilder: self.minimum = self.library.collection_minimum self.current_time = datetime.now() self.current_year = self.current_time.year + self.exists = False + self.created = False methods = {m.lower(): m for m in self.data} @@ -537,6 +547,7 @@ class CollectionBuilder: elif not self.library.Sonarr and "sonarr" in method_name: raise Failed(f"Collection Error: {method_final} requires Sonarr to be configured") elif not self.library.Tautulli and "tautulli" in method_name: raise Failed(f"Collection Error: {method_final} requires Tautulli to be configured") elif not self.config.MyAnimeList and "mal" in method_name: raise Failed(f"Collection Error: {method_final} requires MyAnimeList to be configured") + elif not self.library.Notifiarr and "notifiarr" in method_name: raise Failed(f"Collection Error: {method_final} requires Notifiarr to be configured") elif self.library.is_movie and method_name in show_only_builders: raise Failed(f"Collection Error: {method_final} attribute only works for show libraries") elif self.library.is_show and method_name in movie_only_builders: raise Failed(f"Collection Error: {method_final} attribute only works for movie libraries") elif self.library.is_show and method_name in plex.movie_only_searches: raise Failed(f"Collection Error: {method_final} plex search only works for movie libraries") @@ -617,6 +628,8 @@ class CollectionBuilder: if self.sync and self.obj: for item in self.library.get_collection_items(self.obj, self.smart_label_collection): self.plex_map[item.ratingKey] = item + if self.obj: + self.exists = True else: self.obj = None self.sync = False @@ -1122,7 +1135,7 @@ class CollectionBuilder: rating_keys.append(input_id) elif id_type == "tmdb" and not self.parts_collection: if input_id in self.library.movie_map: - rating_keys.append(self.library.movie_map[input_id][0]) + rating_keys.extend(self.library.movie_map[input_id]) elif input_id not in self.missing_movies: self.missing_movies.append(input_id) elif id_type in ["tvdb", "tmdb_show"] and not self.parts_collection: @@ -1133,12 +1146,12 @@ class CollectionBuilder: logger.error(e) continue if input_id in self.library.show_map: - rating_keys.append(self.library.show_map[input_id][0]) + rating_keys.extend(self.library.show_map[input_id]) elif input_id not in self.missing_shows: self.missing_shows.append(input_id) elif id_type == "imdb" and not self.parts_collection: if input_id in self.library.imdb_map: - rating_keys.append(self.library.imdb_map[input_id][0]) + rating_keys.extend(self.library.imdb_map[input_id]) else: if self.do_missing: try: @@ -1486,6 +1499,14 @@ class CollectionBuilder: self.plex_map[current.ratingKey] = None else: self.library.alter_collection(current, name, smart_label_collection=self.smart_label_collection) + if self.details["notifiarr_collection_addition"]: + if self.library.is_movie and current.ratingKey in self.library.movie_rating_key_map: + add_id = self.library.movie_rating_key_map[current.ratingKey] + elif self.library.is_show and current.ratingKey in self.library.show_rating_key_map: + add_id = self.library.show_rating_key_map[current.ratingKey] + else: + add_id = None + self.notifiarr_additions.append({"title": current.title, "id": add_id}) util.print_end() logger.info("") logger.info(f"{total} {self.collection_level.capitalize()}{'s' if total > 1 else ''} Processed") @@ -1714,6 +1735,14 @@ class CollectionBuilder: self.library.reload(item) logger.info(f"{self.name} Collection | - | {self.item_title(item)}") self.library.alter_collection(item, self.name, smart_label_collection=self.smart_label_collection, add=False) + if self.details["notifiarr_collection_removing"]: + if self.library.is_movie and item.ratingKey in self.library.movie_rating_key_map: + remove_id = self.library.movie_rating_key_map[item.ratingKey] + elif self.library.is_show and item.ratingKey in self.library.show_rating_key_map: + remove_id = self.library.show_rating_key_map[item.ratingKey] + else: + remove_id = None + self.notifiarr_removals.append({"title": item.title, "id": remove_id}) count_removed += 1 if count_removed > 0: logger.info("") @@ -1835,6 +1864,8 @@ class CollectionBuilder: except Failed: raise Failed(f"Collection Error: Label: {self.name} was not added to any items in the Library") self.obj = self.library.get_collection(self.name) + if not self.exists: + self.created = True def update_details(self): logger.info("") @@ -2002,10 +2033,26 @@ class CollectionBuilder: self.library.move_item(self.obj, key, after=previous) previous = key + def send_notifications(self): + if self.obj and ( + (self.details["notifiarr_collection_creation"] and self.created) or + (self.details["notifiarr_collection_addition"] and len(self.notifiarr_additions) > 0) or + (self.details["notifiarr_collection_removing"] and len(self.notifiarr_removals) > 0) + ): + self.obj.reload() + self.library.Notifiarr.plex_collection( + self.obj, + created=self.created, + additions=self.notifiarr_additions, + removals=self.notifiarr_removals + ) + def run_collections_again(self): self.obj = self.library.get_collection(self.name) name, collection_items = self.library.get_collection_name_and_items(self.obj, self.smart_label_collection) + self.created = False rating_keys = [] + self.notifiarr_additions = [] for mm in self.run_again_movies: if mm in self.library.movie_map: rating_keys.extend(self.library.movie_map[mm]) @@ -2025,6 +2072,14 @@ class CollectionBuilder: else: self.library.alter_collection(current, name, smart_label_collection=self.smart_label_collection) logger.info(f"{name} Collection | + | {self.item_title(current)}") + if self.library.is_movie and current.ratingKey in self.library.movie_rating_key_map: + add_id = self.library.movie_rating_key_map[current.ratingKey] + elif self.library.is_show and current.ratingKey in self.library.show_rating_key_map: + add_id = self.library.show_rating_key_map[current.ratingKey] + else: + add_id = None + self.notifiarr_additions.append({"title": current.title, "id": add_id}) + self.send_notifications() logger.info(f"{len(rating_keys)} {self.collection_level.capitalize()}{'s' if len(rating_keys) > 1 else ''} Processed") if len(self.run_again_movies) > 0: diff --git a/modules/config.py b/modules/config.py index c2b8ff36..e8e8477f 100644 --- a/modules/config.py +++ b/modules/config.py @@ -1,4 +1,4 @@ -import logging, os, requests +import base64, logging, os, requests from datetime import datetime from lxml import html from modules import util, radarr, sonarr @@ -10,6 +10,7 @@ from modules.icheckmovies import ICheckMovies from modules.imdb import IMDb from modules.letterboxd import Letterboxd from modules.mal import MyAnimeList +from modules.notifiarr import NotifiarrFactory from modules.omdb import OMDb from modules.plex import Plex from modules.radarr import Radarr @@ -29,21 +30,22 @@ sync_modes = {"append": "Only Add Items to the Collection", "sync": "Add & Remov mass_update_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb"} class Config: - def __init__(self, default_dir, config_path=None, is_test=False, time_scheduled=None, requested_collections=None, requested_libraries=None, resume_from=None): + def __init__(self, default_dir, attrs): logger.info("Locating config...") - if config_path and os.path.exists(config_path): self.config_path = os.path.abspath(config_path) - elif config_path and not os.path.exists(config_path): raise Failed(f"Config Error: config not found at {os.path.abspath(config_path)}") + config_file = attrs["config_file"] + if config_file and os.path.exists(config_file): self.config_path = os.path.abspath(config_file) + elif config_file and not os.path.exists(config_file): raise Failed(f"Config Error: config not found at {os.path.abspath(config_file)}") elif os.path.exists(os.path.join(default_dir, "config.yml")): self.config_path = os.path.abspath(os.path.join(default_dir, "config.yml")) else: raise Failed(f"Config Error: config not found at {os.path.abspath(default_dir)}") logger.info(f"Using {self.config_path} as config") self.default_dir = default_dir - self.test_mode = is_test - self.run_start_time = time_scheduled - self.run_hour = datetime.strptime(time_scheduled, "%H:%M").hour - self.requested_collections = util.get_list(requested_collections) - self.requested_libraries = util.get_list(requested_libraries) - self.resume_from = resume_from + self.test_mode = attrs["test"] + self.run_start_time = attrs["time"] + self.run_hour = datetime.strptime(attrs["time"], "%H:%M").hour + self.requested_collections = util.get_list(attrs["collections"]) + self.requested_libraries = util.get_list(attrs["libraries"]) + self.resume_from = attrs["resume"] yaml.YAML().allow_duplicate_keys = True try: @@ -87,6 +89,7 @@ class Config: if "radarr" in new_config: new_config["radarr"] = new_config.pop("radarr") if "sonarr" in new_config: new_config["sonarr"] = new_config.pop("sonarr") if "omdb" in new_config: new_config["omdb"] = new_config.pop("omdb") + if "notifiarr" in new_config: new_config["notifiarr"] = new_config.pop("notifiarr") if "trakt" in new_config: new_config["trakt"] = new_config.pop("trakt") if "mal" in new_config: new_config["mal"] = new_config.pop("mal") if "anidb" in new_config: new_config["anidb"] = new_config.pop("anidb") @@ -186,7 +189,10 @@ class Config: "missing_only_released": check_for_attribute(self.data, "missing_only_released", parent="settings", var_type="bool", default=False), "create_asset_folders": check_for_attribute(self.data, "create_asset_folders", parent="settings", var_type="bool", default=False), "collection_minimum": check_for_attribute(self.data, "collection_minimum", parent="settings", var_type="int", default=1), - "delete_below_minimum": check_for_attribute(self.data, "delete_below_minimum", parent="settings", var_type="bool", default=False) + "delete_below_minimum": check_for_attribute(self.data, "delete_below_minimum", parent="settings", var_type="bool", default=False), + "notifiarr_collection_creation": check_for_attribute(self.data, "notifiarr_collection_creation", parent="settings", var_type="bool", default=False), + "notifiarr_collection_addition": check_for_attribute(self.data, "notifiarr_collection_addition", parent="settings", var_type="bool", default=False), + "notifiarr_collection_removing": check_for_attribute(self.data, "notifiarr_collection_removing", parent="settings", var_type="bool", default=False) } if self.general["cache"]: util.separator() @@ -196,323 +202,383 @@ class Config: util.separator() - self.TMDb = None - if "tmdb" in self.data: - logger.info("Connecting to TMDb...") - self.TMDb = TMDb(self, { - "apikey": check_for_attribute(self.data, "apikey", parent="tmdb", throw=True), - "language": check_for_attribute(self.data, "language", parent="tmdb", default="en") - }) - logger.info(f"TMDb Connection {'Failed' if self.TMDb is None else 'Successful'}") - else: - raise Failed("Config Error: tmdb attribute not found") - - util.separator() - - self.OMDb = None - if "omdb" in self.data: - logger.info("Connecting to OMDb...") - try: - self.OMDb = OMDb(self, {"apikey": check_for_attribute(self.data, "apikey", parent="omdb", throw=True)}) - except Failed as e: - logger.error(e) - logger.info(f"OMDb Connection {'Failed' if self.OMDb is None else 'Successful'}") - else: - logger.warning("omdb attribute not found") - - util.separator() - - self.Trakt = None - if "trakt" in self.data: - logger.info("Connecting to Trakt...") + self.NotifiarrFactory = None + if "notifiarr" in self.data: + logger.info("Connecting to Notifiarr...") try: - self.Trakt = Trakt(self, { - "client_id": check_for_attribute(self.data, "client_id", parent="trakt", throw=True), - "client_secret": check_for_attribute(self.data, "client_secret", parent="trakt", throw=True), - "config_path": self.config_path, - "authorization": self.data["trakt"]["authorization"] if "authorization" in self.data["trakt"] else None + self.NotifiarrFactory = NotifiarrFactory(self, { + "apikey": check_for_attribute(self.data, "apikey", parent="notifiarr", throw=True), + "error_notification": check_for_attribute(self.data, "error_notification", parent="notifiarr", var_type="bool", default=True), + "develop": check_for_attribute(self.data, "develop", parent="notifiarr", var_type="bool", default=False, do_print=False, save=False), + "test": check_for_attribute(self.data, "test", parent="notifiarr", var_type="bool", default=False, do_print=False, save=False) }) except Failed as e: logger.error(e) - logger.info(f"Trakt Connection {'Failed' if self.Trakt is None else 'Successful'}") + logger.info(f"Notifiarr Connection {'Failed' if self.NotifiarrFactory is None else 'Successful'}") else: - logger.warning("trakt attribute not found") + logger.warning("notifiarr attribute not found") + + self.errors = [] util.separator() - self.MyAnimeList = None - if "mal" in self.data: - logger.info("Connecting to My Anime List...") - try: - self.MyAnimeList = MyAnimeList(self, { - "client_id": check_for_attribute(self.data, "client_id", parent="mal", throw=True), - "client_secret": check_for_attribute(self.data, "client_secret", parent="mal", throw=True), - "config_path": self.config_path, - "authorization": self.data["mal"]["authorization"] if "authorization" in self.data["mal"] else None + try: + self.TMDb = None + if "tmdb" in self.data: + logger.info("Connecting to TMDb...") + self.TMDb = TMDb(self, { + "apikey": check_for_attribute(self.data, "apikey", parent="tmdb", throw=True), + "language": check_for_attribute(self.data, "language", parent="tmdb", default="en") }) - except Failed as e: - logger.error(e) - logger.info(f"My Anime List Connection {'Failed' if self.MyAnimeList is None else 'Successful'}") - else: - logger.warning("mal attribute not found") + logger.info(f"TMDb Connection {'Failed' if self.TMDb is None else 'Successful'}") + else: + raise Failed("Config Error: tmdb attribute not found") - util.separator() + util.separator() + + self.OMDb = None + if "omdb" in self.data: + logger.info("Connecting to OMDb...") + try: + self.OMDb = OMDb(self, {"apikey": check_for_attribute(self.data, "apikey", parent="omdb", throw=True)}) + except Failed as e: + self.errors.append(e) + logger.error(e) + logger.info(f"OMDb Connection {'Failed' if self.OMDb is None else 'Successful'}") + else: + logger.warning("omdb attribute not found") - self.AniDB = None - if "anidb" in self.data: util.separator() - logger.info("Connecting to AniDB...") - try: - self.AniDB = AniDB(self, { - "username": check_for_attribute(self.data, "username", parent="anidb", throw=True), - "password": check_for_attribute(self.data, "password", parent="anidb", throw=True) + + self.Trakt = None + if "trakt" in self.data: + logger.info("Connecting to Trakt...") + try: + self.Trakt = Trakt(self, { + "client_id": check_for_attribute(self.data, "client_id", parent="trakt", throw=True), + "client_secret": check_for_attribute(self.data, "client_secret", parent="trakt", throw=True), + "config_path": self.config_path, + "authorization": self.data["trakt"]["authorization"] if "authorization" in self.data["trakt"] else None }) - except Failed as e: - logger.error(e) - logger.info(f"My Anime List Connection {'Failed Continuing as Guest ' if self.MyAnimeList is None else 'Successful'}") - if self.AniDB is None: - self.AniDB = AniDB(self, None) - - self.TVDb = TVDb(self) - self.IMDb = IMDb(self) - self.Convert = Convert(self) - self.AniList = AniList(self) - self.Letterboxd = Letterboxd(self) - self.ICheckMovies = ICheckMovies(self) - self.StevenLu = StevenLu(self) + except Failed as e: + self.errors.append(e) + logger.error(e) + logger.info(f"Trakt Connection {'Failed' if self.Trakt is None else 'Successful'}") + else: + logger.warning("trakt attribute not found") - util.separator() + util.separator() - logger.info("Connecting to Plex Libraries...") + self.MyAnimeList = None + if "mal" in self.data: + logger.info("Connecting to My Anime List...") + try: + self.MyAnimeList = MyAnimeList(self, { + "client_id": check_for_attribute(self.data, "client_id", parent="mal", throw=True), + "client_secret": check_for_attribute(self.data, "client_secret", parent="mal", throw=True), + "config_path": self.config_path, + "authorization": self.data["mal"]["authorization"] if "authorization" in self.data["mal"] else None + }) + except Failed as e: + self.errors.append(e) + logger.error(e) + logger.info(f"My Anime List Connection {'Failed' if self.MyAnimeList is None else 'Successful'}") + else: + logger.warning("mal attribute not found") - self.general["plex"] = { - "url": check_for_attribute(self.data, "url", parent="plex", var_type="url", default_is_none=True), - "token": check_for_attribute(self.data, "token", parent="plex", default_is_none=True), - "timeout": check_for_attribute(self.data, "timeout", parent="plex", var_type="int", default=60), - "clean_bundles": check_for_attribute(self.data, "clean_bundles", parent="plex", var_type="bool", default=False), - "empty_trash": check_for_attribute(self.data, "empty_trash", parent="plex", var_type="bool", default=False), - "optimize": check_for_attribute(self.data, "optimize", parent="plex", var_type="bool", default=False) - } - self.general["radarr"] = { - "url": check_for_attribute(self.data, "url", parent="radarr", var_type="url", default_is_none=True), - "token": check_for_attribute(self.data, "token", parent="radarr", default_is_none=True), - "add": check_for_attribute(self.data, "add", parent="radarr", var_type="bool", default=False), - "add_existing": check_for_attribute(self.data, "add_existing", parent="radarr", var_type="bool", default=False), - "root_folder_path": check_for_attribute(self.data, "root_folder_path", parent="radarr", default_is_none=True), - "monitor": check_for_attribute(self.data, "monitor", parent="radarr", var_type="bool", default=True), - "availability": check_for_attribute(self.data, "availability", parent="radarr", test_list=radarr.availability_descriptions, default="announced"), - "quality_profile": check_for_attribute(self.data, "quality_profile", parent="radarr", default_is_none=True), - "tag": check_for_attribute(self.data, "tag", parent="radarr", var_type="lower_list", default_is_none=True), - "search": check_for_attribute(self.data, "search", parent="radarr", var_type="bool", default=False) - } - self.general["sonarr"] = { - "url": check_for_attribute(self.data, "url", parent="sonarr", var_type="url", default_is_none=True), - "token": check_for_attribute(self.data, "token", parent="sonarr", default_is_none=True), - "add": check_for_attribute(self.data, "add", parent="sonarr", var_type="bool", default=False), - "add_existing": check_for_attribute(self.data, "add_existing", parent="sonarr", var_type="bool", default=False), - "root_folder_path": check_for_attribute(self.data, "root_folder_path", parent="sonarr", default_is_none=True), - "monitor": check_for_attribute(self.data, "monitor", parent="sonarr", test_list=sonarr.monitor_descriptions, default="all"), - "quality_profile": check_for_attribute(self.data, "quality_profile", parent="sonarr", default_is_none=True), - "language_profile": check_for_attribute(self.data, "language_profile", parent="sonarr", default_is_none=True), - "series_type": check_for_attribute(self.data, "series_type", parent="sonarr", test_list=sonarr.series_type_descriptions, default="standard"), - "season_folder": check_for_attribute(self.data, "season_folder", parent="sonarr", var_type="bool", default=True), - "tag": check_for_attribute(self.data, "tag", parent="sonarr", var_type="lower_list", default_is_none=True), - "search": check_for_attribute(self.data, "search", parent="sonarr", var_type="bool", default=False), - "cutoff_search": check_for_attribute(self.data, "cutoff_search", parent="sonarr", var_type="bool", default=False) - } - self.general["tautulli"] = { - "url": check_for_attribute(self.data, "url", parent="tautulli", var_type="url", default_is_none=True), - "apikey": check_for_attribute(self.data, "apikey", parent="tautulli", default_is_none=True) - } + util.separator() - self.libraries = [] - libs = check_for_attribute(self.data, "libraries", throw=True) + self.AniDB = None + if "anidb" in self.data: + util.separator() + logger.info("Connecting to AniDB...") + try: + self.AniDB = AniDB(self, { + "username": check_for_attribute(self.data, "username", parent="anidb", throw=True), + "password": check_for_attribute(self.data, "password", parent="anidb", throw=True) + }) + except Failed as e: + self.errors.append(e) + logger.error(e) + logger.info(f"My Anime List Connection {'Failed Continuing as Guest ' if self.MyAnimeList is None else 'Successful'}") + if self.AniDB is None: + self.AniDB = AniDB(self, None) + + self.TVDb = TVDb(self) + self.IMDb = IMDb(self) + self.Convert = Convert(self) + self.AniList = AniList(self) + self.Letterboxd = Letterboxd(self) + self.ICheckMovies = ICheckMovies(self) + self.StevenLu = StevenLu(self) - for library_name, lib in libs.items(): - if self.requested_libraries and library_name not in self.requested_libraries: - continue util.separator() - params = { - "mapping_name": str(library_name), - "name": str(lib["library_name"]) if lib and "library_name" in lib and lib["library_name"] else str(library_name) + + logger.info("Connecting to Plex Libraries...") + + self.general["plex"] = { + "url": check_for_attribute(self.data, "url", parent="plex", var_type="url", default_is_none=True), + "token": check_for_attribute(self.data, "token", parent="plex", default_is_none=True), + "timeout": check_for_attribute(self.data, "timeout", parent="plex", var_type="int", default=60), + "clean_bundles": check_for_attribute(self.data, "clean_bundles", parent="plex", var_type="bool", default=False), + "empty_trash": check_for_attribute(self.data, "empty_trash", parent="plex", var_type="bool", default=False), + "optimize": check_for_attribute(self.data, "optimize", parent="plex", var_type="bool", default=False) + } + self.general["radarr"] = { + "url": check_for_attribute(self.data, "url", parent="radarr", var_type="url", default_is_none=True), + "token": check_for_attribute(self.data, "token", parent="radarr", default_is_none=True), + "add": check_for_attribute(self.data, "add", parent="radarr", var_type="bool", default=False), + "add_existing": check_for_attribute(self.data, "add_existing", parent="radarr", var_type="bool", default=False), + "root_folder_path": check_for_attribute(self.data, "root_folder_path", parent="radarr", default_is_none=True), + "monitor": check_for_attribute(self.data, "monitor", parent="radarr", var_type="bool", default=True), + "availability": check_for_attribute(self.data, "availability", parent="radarr", test_list=radarr.availability_descriptions, default="announced"), + "quality_profile": check_for_attribute(self.data, "quality_profile", parent="radarr", default_is_none=True), + "tag": check_for_attribute(self.data, "tag", parent="radarr", var_type="lower_list", default_is_none=True), + "search": check_for_attribute(self.data, "search", parent="radarr", var_type="bool", default=False) + } + self.general["sonarr"] = { + "url": check_for_attribute(self.data, "url", parent="sonarr", var_type="url", default_is_none=True), + "token": check_for_attribute(self.data, "token", parent="sonarr", default_is_none=True), + "add": check_for_attribute(self.data, "add", parent="sonarr", var_type="bool", default=False), + "add_existing": check_for_attribute(self.data, "add_existing", parent="sonarr", var_type="bool", default=False), + "root_folder_path": check_for_attribute(self.data, "root_folder_path", parent="sonarr", default_is_none=True), + "monitor": check_for_attribute(self.data, "monitor", parent="sonarr", test_list=sonarr.monitor_descriptions, default="all"), + "quality_profile": check_for_attribute(self.data, "quality_profile", parent="sonarr", default_is_none=True), + "language_profile": check_for_attribute(self.data, "language_profile", parent="sonarr", default_is_none=True), + "series_type": check_for_attribute(self.data, "series_type", parent="sonarr", test_list=sonarr.series_type_descriptions, default="standard"), + "season_folder": check_for_attribute(self.data, "season_folder", parent="sonarr", var_type="bool", default=True), + "tag": check_for_attribute(self.data, "tag", parent="sonarr", var_type="lower_list", default_is_none=True), + "search": check_for_attribute(self.data, "search", parent="sonarr", var_type="bool", default=False), + "cutoff_search": check_for_attribute(self.data, "cutoff_search", parent="sonarr", var_type="bool", default=False) + } + self.general["tautulli"] = { + "url": check_for_attribute(self.data, "url", parent="tautulli", var_type="url", default_is_none=True), + "apikey": check_for_attribute(self.data, "apikey", parent="tautulli", default_is_none=True) } - display_name = f"{params['name']} ({params['mapping_name']})" if lib and "library_name" in lib and lib["library_name"] else params["mapping_name"] - - util.separator(f"{display_name} Configuration") - logger.info("") - logger.info(f"Connecting to {display_name} Library...") - - params["asset_directory"] = check_for_attribute(lib, "asset_directory", parent="settings", var_type="list_path", default=self.general["asset_directory"], default_is_none=True, save=False) - if params["asset_directory"] is None: - logger.warning("Config Warning: Assets will not be used asset_directory attribute must be set under config or under this specific Library") - - params["asset_folders"] = check_for_attribute(lib, "asset_folders", parent="settings", var_type="bool", default=self.general["asset_folders"], do_print=False, save=False) - params["assets_for_all"] = check_for_attribute(lib, "assets_for_all", parent="settings", var_type="bool", default=self.general["assets_for_all"], do_print=False, save=False) - params["sync_mode"] = check_for_attribute(lib, "sync_mode", parent="settings", test_list=sync_modes, default=self.general["sync_mode"], do_print=False, save=False) - params["show_unmanaged"] = check_for_attribute(lib, "show_unmanaged", parent="settings", var_type="bool", default=self.general["show_unmanaged"], do_print=False, save=False) - params["show_filtered"] = check_for_attribute(lib, "show_filtered", parent="settings", var_type="bool", default=self.general["show_filtered"], do_print=False, save=False) - params["show_missing"] = check_for_attribute(lib, "show_missing", parent="settings", var_type="bool", default=self.general["show_missing"], do_print=False, save=False) - params["save_missing"] = check_for_attribute(lib, "save_missing", parent="settings", var_type="bool", default=self.general["save_missing"], do_print=False, save=False) - params["missing_only_released"] = check_for_attribute(lib, "missing_only_released", parent="settings", var_type="bool", default=self.general["missing_only_released"], do_print=False, save=False) - params["create_asset_folders"] = check_for_attribute(lib, "create_asset_folders", parent="settings", var_type="bool", default=self.general["create_asset_folders"], do_print=False, save=False) - params["collection_minimum"] = check_for_attribute(lib, "collection_minimum", parent="settings", var_type="int", default=self.general["collection_minimum"], do_print=False, save=False) - params["delete_below_minimum"] = check_for_attribute(lib, "delete_below_minimum", parent="settings", var_type="bool", default=self.general["delete_below_minimum"], do_print=False, save=False) - - params["mass_genre_update"] = check_for_attribute(lib, "mass_genre_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=lib and "mass_genre_update" in lib) - if self.OMDb is None and params["mass_genre_update"] == "omdb": - params["mass_genre_update"] = None - logger.error("Config Error: mass_genre_update cannot be omdb without a successful OMDb Connection") - - params["mass_audience_rating_update"] = check_for_attribute(lib, "mass_audience_rating_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=lib and "mass_audience_rating_update" in lib) - if self.OMDb is None and params["mass_audience_rating_update"] == "omdb": - params["mass_audience_rating_update"] = None - logger.error("Config Error: mass_audience_rating_update cannot be omdb without a successful OMDb Connection") - - params["mass_critic_rating_update"] = check_for_attribute(lib, "mass_critic_rating_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=lib and "mass_audience_rating_update" in lib) - if self.OMDb is None and params["mass_critic_rating_update"] == "omdb": - params["mass_critic_rating_update"] = None - logger.error("Config Error: mass_critic_rating_update cannot be omdb without a successful OMDb Connection") - - params["mass_trakt_rating_update"] = check_for_attribute(lib, "mass_trakt_rating_update", var_type="bool", default=False, save=False, do_print=lib and "mass_trakt_rating_update" in lib) - if self.Trakt is None and params["mass_trakt_rating_update"]: - params["mass_trakt_rating_update"] = None - logger.error("Config Error: mass_trakt_rating_update cannot run without a successful Trakt Connection") - - params["split_duplicates"] = check_for_attribute(lib, "split_duplicates", var_type="bool", default=False, save=False, do_print=lib and "split_duplicates" in lib) - params["radarr_add_all"] = check_for_attribute(lib, "radarr_add_all", var_type="bool", default=False, save=False, do_print=lib and "radarr_add_all" in lib) - params["sonarr_add_all"] = check_for_attribute(lib, "sonarr_add_all", var_type="bool", default=False, save=False, do_print=lib and "sonarr_add_all" in lib) - try: - if lib and "metadata_path" in lib: - params["metadata_path"] = [] - if lib["metadata_path"] is None: - raise Failed("Config Error: metadata_path attribute is blank") - paths_to_check = lib["metadata_path"] if isinstance(lib["metadata_path"], list) else [lib["metadata_path"]] - for path in paths_to_check: - if isinstance(path, dict): - def check_dict(attr, name): - if attr in path: - if path[attr] is None: - logger.error(f"Config Error: metadata_path {attr} is blank") - else: - params["metadata_path"].append((name, path[attr])) - check_dict("url", "URL") - check_dict("git", "Git") - check_dict("file", "File") - check_dict("folder", "Folder") - else: - params["metadata_path"].append(("File", path)) - else: - params["metadata_path"] = [("File", os.path.join(default_dir, f"{library_name}.yml"))] - params["default_dir"] = default_dir - params["plex"] = { - "url": check_for_attribute(lib, "url", parent="plex", var_type="url", default=self.general["plex"]["url"], req_default=True, save=False), - "token": check_for_attribute(lib, "token", parent="plex", default=self.general["plex"]["token"], req_default=True, save=False), - "timeout": check_for_attribute(lib, "timeout", parent="plex", var_type="int", default=self.general["plex"]["timeout"], save=False), - "clean_bundles": check_for_attribute(lib, "clean_bundles", parent="plex", var_type="bool", default=self.general["plex"]["clean_bundles"], save=False), - "empty_trash": check_for_attribute(lib, "empty_trash", parent="plex", var_type="bool", default=self.general["plex"]["empty_trash"], save=False), - "optimize": check_for_attribute(lib, "optimize", parent="plex", var_type="bool", default=self.general["plex"]["optimize"], save=False) + self.libraries = [] + libs = check_for_attribute(self.data, "libraries", throw=True) + + for library_name, lib in libs.items(): + if self.requested_libraries and library_name not in self.requested_libraries: + continue + util.separator() + params = { + "mapping_name": str(library_name), + "name": str(lib["library_name"]) if lib and "library_name" in lib and lib["library_name"] else str(library_name) } - library = Plex(self, params) - logger.info("") - logger.info(f"{display_name} Library Connection Successful") - except Failed as e: - util.print_stacktrace() - util.print_multiline(e, error=True) - logger.info(f"{display_name} Library Connection Failed") - continue + display_name = f"{params['name']} ({params['mapping_name']})" if lib and "library_name" in lib and lib["library_name"] else params["mapping_name"] - if self.general["radarr"]["url"] or (lib and "radarr" in lib): + util.separator(f"{display_name} Configuration") logger.info("") - util.separator("Radarr Configuration", space=False, border=False) - logger.info("") - logger.info(f"Connecting to {display_name} library's Radarr...") - logger.info("") - try: - library.Radarr = Radarr(self, { - "url": check_for_attribute(lib, "url", parent="radarr", var_type="url", default=self.general["radarr"]["url"], req_default=True, save=False), - "token": check_for_attribute(lib, "token", parent="radarr", default=self.general["radarr"]["token"], req_default=True, save=False), - "add": check_for_attribute(lib, "add", parent="radarr", var_type="bool", default=self.general["radarr"]["add"], save=False), - "add_existing": check_for_attribute(lib, "add_existing", parent="radarr", var_type="bool", default=self.general["radarr"]["add_existing"], save=False), - "root_folder_path": check_for_attribute(lib, "root_folder_path", parent="radarr", default=self.general["radarr"]["root_folder_path"], req_default=True, save=False), - "monitor": check_for_attribute(lib, "monitor", parent="radarr", var_type="bool", default=self.general["radarr"]["monitor"], save=False), - "availability": check_for_attribute(lib, "availability", parent="radarr", test_list=radarr.availability_descriptions, default=self.general["radarr"]["availability"], save=False), - "quality_profile": check_for_attribute(lib, "quality_profile", parent="radarr",default=self.general["radarr"]["quality_profile"], req_default=True, save=False), - "tag": check_for_attribute(lib, "tag", parent="radarr", var_type="lower_list", default=self.general["radarr"]["tag"], default_is_none=True, save=False), - "search": check_for_attribute(lib, "search", parent="radarr", var_type="bool", default=self.general["radarr"]["search"], save=False) - }) - except Failed as e: - util.print_stacktrace() - util.print_multiline(e, error=True) - logger.info("") - logger.info(f"{display_name} library's Radarr Connection {'Failed' if library.Radarr is None else 'Successful'}") + logger.info(f"Connecting to {display_name} Library...") + + params["asset_directory"] = check_for_attribute(lib, "asset_directory", parent="settings", var_type="list_path", default=self.general["asset_directory"], default_is_none=True, save=False) + if params["asset_directory"] is None: + logger.warning("Config Warning: Assets will not be used asset_directory attribute must be set under config or under this specific Library") + + params["asset_folders"] = check_for_attribute(lib, "asset_folders", parent="settings", var_type="bool", default=self.general["asset_folders"], do_print=False, save=False) + params["assets_for_all"] = check_for_attribute(lib, "assets_for_all", parent="settings", var_type="bool", default=self.general["assets_for_all"], do_print=False, save=False) + params["sync_mode"] = check_for_attribute(lib, "sync_mode", parent="settings", test_list=sync_modes, default=self.general["sync_mode"], do_print=False, save=False) + params["show_unmanaged"] = check_for_attribute(lib, "show_unmanaged", parent="settings", var_type="bool", default=self.general["show_unmanaged"], do_print=False, save=False) + params["show_filtered"] = check_for_attribute(lib, "show_filtered", parent="settings", var_type="bool", default=self.general["show_filtered"], do_print=False, save=False) + params["show_missing"] = check_for_attribute(lib, "show_missing", parent="settings", var_type="bool", default=self.general["show_missing"], do_print=False, save=False) + params["save_missing"] = check_for_attribute(lib, "save_missing", parent="settings", var_type="bool", default=self.general["save_missing"], do_print=False, save=False) + params["missing_only_released"] = check_for_attribute(lib, "missing_only_released", parent="settings", var_type="bool", default=self.general["missing_only_released"], do_print=False, save=False) + params["create_asset_folders"] = check_for_attribute(lib, "create_asset_folders", parent="settings", var_type="bool", default=self.general["create_asset_folders"], do_print=False, save=False) + params["collection_minimum"] = check_for_attribute(lib, "collection_minimum", parent="settings", var_type="int", default=self.general["collection_minimum"], do_print=False, save=False) + params["delete_below_minimum"] = check_for_attribute(lib, "delete_below_minimum", parent="settings", var_type="bool", default=self.general["delete_below_minimum"], do_print=False, save=False) + params["notifiarr_collection_creation"] = check_for_attribute(lib, "notifiarr_collection_creation", parent="settings", var_type="bool", default=self.general["notifiarr_collection_creation"], do_print=False, save=False) + params["notifiarr_collection_addition"] = check_for_attribute(lib, "notifiarr_collection_addition", parent="settings", var_type="bool", default=self.general["notifiarr_collection_addition"], do_print=False, save=False) + params["notifiarr_collection_removing"] = check_for_attribute(lib, "notifiarr_collection_removing", parent="settings", var_type="bool", default=self.general["notifiarr_collection_removing"], do_print=False, save=False) + + params["mass_genre_update"] = check_for_attribute(lib, "mass_genre_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=lib and "mass_genre_update" in lib) + if self.OMDb is None and params["mass_genre_update"] == "omdb": + params["mass_genre_update"] = None + e = "Config Error: mass_genre_update cannot be omdb without a successful OMDb Connection" + self.errors.append(e) + logger.error(e) + + params["mass_audience_rating_update"] = check_for_attribute(lib, "mass_audience_rating_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=lib and "mass_audience_rating_update" in lib) + if self.OMDb is None and params["mass_audience_rating_update"] == "omdb": + params["mass_audience_rating_update"] = None + e = "Config Error: mass_audience_rating_update cannot be omdb without a successful OMDb Connection" + self.errors.append(e) + logger.error(e) + + params["mass_critic_rating_update"] = check_for_attribute(lib, "mass_critic_rating_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=lib and "mass_audience_rating_update" in lib) + if self.OMDb is None and params["mass_critic_rating_update"] == "omdb": + params["mass_critic_rating_update"] = None + e = "Config Error: mass_critic_rating_update cannot be omdb without a successful OMDb Connection" + self.errors.append(e) + logger.error(e) + + params["mass_trakt_rating_update"] = check_for_attribute(lib, "mass_trakt_rating_update", var_type="bool", default=False, save=False, do_print=lib and "mass_trakt_rating_update" in lib) + if self.Trakt is None and params["mass_trakt_rating_update"]: + params["mass_trakt_rating_update"] = None + e = "Config Error: mass_trakt_rating_update cannot run without a successful Trakt Connection" + self.errors.append(e) + logger.error(e) + + params["split_duplicates"] = check_for_attribute(lib, "split_duplicates", var_type="bool", default=False, save=False, do_print=lib and "split_duplicates" in lib) + params["radarr_add_all"] = check_for_attribute(lib, "radarr_add_all", var_type="bool", default=False, save=False, do_print=lib and "radarr_add_all" in lib) + params["sonarr_add_all"] = check_for_attribute(lib, "sonarr_add_all", var_type="bool", default=False, save=False, do_print=lib and "sonarr_add_all" in lib) - if self.general["sonarr"]["url"] or (lib and "sonarr" in lib): - logger.info("") - util.separator("Sonarr Configuration", space=False, border=False) - logger.info("") - logger.info(f"Connecting to {display_name} library's Sonarr...") - logger.info("") try: - library.Sonarr = Sonarr(self, { - "url": check_for_attribute(lib, "url", parent="sonarr", var_type="url", default=self.general["sonarr"]["url"], req_default=True, save=False), - "token": check_for_attribute(lib, "token", parent="sonarr", default=self.general["sonarr"]["token"], req_default=True, save=False), - "add": check_for_attribute(lib, "add", parent="sonarr", var_type="bool", default=self.general["sonarr"]["add"], save=False), - "add_existing": check_for_attribute(lib, "add_existing", parent="sonarr", var_type="bool", default=self.general["sonarr"]["add_existing"], save=False), - "root_folder_path": check_for_attribute(lib, "root_folder_path", parent="sonarr", default=self.general["sonarr"]["root_folder_path"], req_default=True, save=False), - "monitor": check_for_attribute(lib, "monitor", parent="sonarr", test_list=sonarr.monitor_descriptions, default=self.general["sonarr"]["monitor"], save=False), - "quality_profile": check_for_attribute(lib, "quality_profile", parent="sonarr", default=self.general["sonarr"]["quality_profile"], req_default=True, save=False), - "language_profile": check_for_attribute(lib, "language_profile", parent="sonarr", default=self.general["sonarr"]["language_profile"], save=False) if self.general["sonarr"]["language_profile"] else check_for_attribute(lib, "language_profile", parent="sonarr", default_is_none=True, save=False), - "series_type": check_for_attribute(lib, "series_type", parent="sonarr", test_list=sonarr.series_type_descriptions, default=self.general["sonarr"]["series_type"], save=False), - "season_folder": check_for_attribute(lib, "season_folder", parent="sonarr", var_type="bool", default=self.general["sonarr"]["season_folder"], save=False), - "tag": check_for_attribute(lib, "tag", parent="sonarr", var_type="lower_list", default=self.general["sonarr"]["tag"], default_is_none=True, save=False), - "search": check_for_attribute(lib, "search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["search"], save=False), - "cutoff_search": check_for_attribute(lib, "cutoff_search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["cutoff_search"], save=False) - }) + if lib and "metadata_path" in lib: + params["metadata_path"] = [] + if lib["metadata_path"] is None: + raise Failed("Config Error: metadata_path attribute is blank") + paths_to_check = lib["metadata_path"] if isinstance(lib["metadata_path"], list) else [lib["metadata_path"]] + for path in paths_to_check: + if isinstance(path, dict): + def check_dict(attr, name): + if attr in path: + if path[attr] is None: + e = f"Config Error: metadata_path {attr} is blank" + self.errors.append(e) + logger.error(e) + else: + params["metadata_path"].append((name, path[attr])) + check_dict("url", "URL") + check_dict("git", "Git") + check_dict("file", "File") + check_dict("folder", "Folder") + else: + params["metadata_path"].append(("File", path)) + else: + params["metadata_path"] = [("File", os.path.join(default_dir, f"{library_name}.yml"))] + params["default_dir"] = default_dir + params["plex"] = { + "url": check_for_attribute(lib, "url", parent="plex", var_type="url", default=self.general["plex"]["url"], req_default=True, save=False), + "token": check_for_attribute(lib, "token", parent="plex", default=self.general["plex"]["token"], req_default=True, save=False), + "timeout": check_for_attribute(lib, "timeout", parent="plex", var_type="int", default=self.general["plex"]["timeout"], save=False), + "clean_bundles": check_for_attribute(lib, "clean_bundles", parent="plex", var_type="bool", default=self.general["plex"]["clean_bundles"], save=False), + "empty_trash": check_for_attribute(lib, "empty_trash", parent="plex", var_type="bool", default=self.general["plex"]["empty_trash"], save=False), + "optimize": check_for_attribute(lib, "optimize", parent="plex", var_type="bool", default=self.general["plex"]["optimize"], save=False) + } + library = Plex(self, params) + logger.info("") + logger.info(f"{display_name} Library Connection Successful") except Failed as e: + self.errors.append(e) util.print_stacktrace() util.print_multiline(e, error=True) + logger.info(f"{display_name} Library Connection Failed") + continue + + if self.general["radarr"]["url"] or (lib and "radarr" in lib): + logger.info("") + util.separator("Radarr Configuration", space=False, border=False) + logger.info("") + logger.info(f"Connecting to {display_name} library's Radarr...") logger.info("") - logger.info(f"{display_name} library's Sonarr Connection {'Failed' if library.Sonarr is None else 'Successful'}") + try: + library.Radarr = Radarr(self, { + "url": check_for_attribute(lib, "url", parent="radarr", var_type="url", default=self.general["radarr"]["url"], req_default=True, save=False), + "token": check_for_attribute(lib, "token", parent="radarr", default=self.general["radarr"]["token"], req_default=True, save=False), + "add": check_for_attribute(lib, "add", parent="radarr", var_type="bool", default=self.general["radarr"]["add"], save=False), + "add_existing": check_for_attribute(lib, "add_existing", parent="radarr", var_type="bool", default=self.general["radarr"]["add_existing"], save=False), + "root_folder_path": check_for_attribute(lib, "root_folder_path", parent="radarr", default=self.general["radarr"]["root_folder_path"], req_default=True, save=False), + "monitor": check_for_attribute(lib, "monitor", parent="radarr", var_type="bool", default=self.general["radarr"]["monitor"], save=False), + "availability": check_for_attribute(lib, "availability", parent="radarr", test_list=radarr.availability_descriptions, default=self.general["radarr"]["availability"], save=False), + "quality_profile": check_for_attribute(lib, "quality_profile", parent="radarr",default=self.general["radarr"]["quality_profile"], req_default=True, save=False), + "tag": check_for_attribute(lib, "tag", parent="radarr", var_type="lower_list", default=self.general["radarr"]["tag"], default_is_none=True, save=False), + "search": check_for_attribute(lib, "search", parent="radarr", var_type="bool", default=self.general["radarr"]["search"], save=False) + }) + except Failed as e: + self.errors.append(e) + util.print_stacktrace() + util.print_multiline(e, error=True) + logger.info("") + logger.info(f"{display_name} library's Radarr Connection {'Failed' if library.Radarr is None else 'Successful'}") + + if self.general["sonarr"]["url"] or (lib and "sonarr" in lib): + logger.info("") + util.separator("Sonarr Configuration", space=False, border=False) + logger.info("") + logger.info(f"Connecting to {display_name} library's Sonarr...") + logger.info("") + try: + library.Sonarr = Sonarr(self, { + "url": check_for_attribute(lib, "url", parent="sonarr", var_type="url", default=self.general["sonarr"]["url"], req_default=True, save=False), + "token": check_for_attribute(lib, "token", parent="sonarr", default=self.general["sonarr"]["token"], req_default=True, save=False), + "add": check_for_attribute(lib, "add", parent="sonarr", var_type="bool", default=self.general["sonarr"]["add"], save=False), + "add_existing": check_for_attribute(lib, "add_existing", parent="sonarr", var_type="bool", default=self.general["sonarr"]["add_existing"], save=False), + "root_folder_path": check_for_attribute(lib, "root_folder_path", parent="sonarr", default=self.general["sonarr"]["root_folder_path"], req_default=True, save=False), + "monitor": check_for_attribute(lib, "monitor", parent="sonarr", test_list=sonarr.monitor_descriptions, default=self.general["sonarr"]["monitor"], save=False), + "quality_profile": check_for_attribute(lib, "quality_profile", parent="sonarr", default=self.general["sonarr"]["quality_profile"], req_default=True, save=False), + "language_profile": check_for_attribute(lib, "language_profile", parent="sonarr", default=self.general["sonarr"]["language_profile"], save=False) if self.general["sonarr"]["language_profile"] else check_for_attribute(lib, "language_profile", parent="sonarr", default_is_none=True, save=False), + "series_type": check_for_attribute(lib, "series_type", parent="sonarr", test_list=sonarr.series_type_descriptions, default=self.general["sonarr"]["series_type"], save=False), + "season_folder": check_for_attribute(lib, "season_folder", parent="sonarr", var_type="bool", default=self.general["sonarr"]["season_folder"], save=False), + "tag": check_for_attribute(lib, "tag", parent="sonarr", var_type="lower_list", default=self.general["sonarr"]["tag"], default_is_none=True, save=False), + "search": check_for_attribute(lib, "search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["search"], save=False), + "cutoff_search": check_for_attribute(lib, "cutoff_search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["cutoff_search"], save=False) + }) + except Failed as e: + self.errors.append(e) + util.print_stacktrace() + util.print_multiline(e, error=True) + logger.info("") + logger.info(f"{display_name} library's Sonarr Connection {'Failed' if library.Sonarr is None else 'Successful'}") + + if self.general["tautulli"]["url"] or (lib and "tautulli" in lib): + logger.info("") + util.separator("Tautulli Configuration", space=False, border=False) + logger.info("") + logger.info(f"Connecting to {display_name} library's Tautulli...") + logger.info("") + try: + library.Tautulli = Tautulli(self, { + "url": check_for_attribute(lib, "url", parent="tautulli", var_type="url", default=self.general["tautulli"]["url"], req_default=True, save=False), + "apikey": check_for_attribute(lib, "apikey", parent="tautulli", default=self.general["tautulli"]["apikey"], req_default=True, save=False) + }) + except Failed as e: + self.errors.append(e) + util.print_stacktrace() + util.print_multiline(e, error=True) + logger.info("") + logger.info(f"{display_name} library's Tautulli Connection {'Failed' if library.Tautulli is None else 'Successful'}") + + library.Notifiarr = self.NotifiarrFactory.getNotifiarr(library) if self.NotifiarrFactory else None - if self.general["tautulli"]["url"] or (lib and "tautulli" in lib): logger.info("") - util.separator("Tautulli Configuration", space=False, border=False) - logger.info("") - logger.info(f"Connecting to {display_name} library's Tautulli...") - logger.info("") - try: - library.Tautulli = Tautulli(self, { - "url": check_for_attribute(lib, "url", parent="tautulli", var_type="url", default=self.general["tautulli"]["url"], req_default=True, save=False), - "apikey": check_for_attribute(lib, "apikey", parent="tautulli", default=self.general["tautulli"]["apikey"], req_default=True, save=False) - }) - except Failed as e: - util.print_stacktrace() - util.print_multiline(e, error=True) - logger.info("") - logger.info(f"{display_name} library's Tautulli Connection {'Failed' if library.Tautulli is None else 'Successful'}") + self.libraries.append(library) - logger.info("") - self.libraries.append(library) + util.separator() - util.separator() + if len(self.libraries) > 0: + logger.info(f"{len(self.libraries)} Plex Library Connection{'s' if len(self.libraries) > 1 else ''} Successful") + else: + raise Failed("Plex Error: No Plex libraries were connected to") - if len(self.libraries) > 0: - logger.info(f"{len(self.libraries)} Plex Library Connection{'s' if len(self.libraries) > 1 else ''} Successful") - else: - raise Failed("Plex Error: No Plex libraries were connected to") + util.separator() - util.separator() + if self.errors: + self.notify(self.errors) + except Exception as e: + self.notify(e) + raise + + def notify(self, text, library=None, collection=None, critical=True): + if self.NotifiarrFactory: + if not isinstance(text, list): + text = [text] + for t in text: + self.NotifiarrFactory.error(t, library=library, collection=collection, critical=critical) def get_html(self, url, headers=None, params=None): return html.fromstring(self.get(url, headers=headers, params=params).content) - def get_json(self, url, headers=None): - return self.get(url, headers=headers).json() + def get_json(self, url, json=None, headers=None, params=None): + return self.get(url, json=json, headers=headers, params=params).json() @retry(stop_max_attempt_number=6, wait_fixed=10000) - def get(self, url, headers=None, params=None): - return self.session.get(url, headers=headers, params=params) + def get(self, url, json=None, headers=None, params=None): + return self.session.get(url, json=json, headers=headers, params=params) + + def get_image_encoded(self, url): + return base64.b64encode(self.get(url).content).decode('utf-8') def post_html(self, url, data=None, json=None, headers=None): return html.fromstring(self.post(url, data=data, json=json, headers=headers).content) diff --git a/modules/imdb.py b/modules/imdb.py index e1fb9f3d..dcc1e917 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -57,7 +57,7 @@ class IMDb: pass if total > 0: return total, item_counts[page_type] - raise ValueError(f"IMDb Error: Failed to parse URL: {imdb_url}") + raise Failed(f"IMDb Error: Failed to parse URL: {imdb_url}") def _ids_from_url(self, imdb_url, language, limit): total, item_count = self._total(imdb_url, language) @@ -93,7 +93,7 @@ class IMDb: if len(imdb_ids) > 0: logger.debug(f"{len(imdb_ids)} IMDb IDs Found: {imdb_ids}") return imdb_ids - raise ValueError(f"IMDb Error: No IMDb IDs Found at {imdb_url}") + raise Failed(f"IMDb Error: No IMDb IDs Found at {imdb_url}") def get_imdb_ids(self, method, data, language): if method == "imdb_id": diff --git a/modules/library.py b/modules/library.py index 7f420253..92cd1b2c 100644 --- a/modules/library.py +++ b/modules/library.py @@ -13,6 +13,7 @@ class Library(ABC): self.Radarr = None self.Sonarr = None self.Tautulli = None + self.Notifiarr = None self.collections = [] self.metadatas = [] self.metadata_files = [] @@ -54,6 +55,9 @@ class Library(ABC): self.sonarr_add_all = params["sonarr_add_all"] self.collection_minimum = params["collection_minimum"] self.delete_below_minimum = params["delete_below_minimum"] + self.notifiarr_collection_creation = params["notifiarr_collection_creation"] + self.notifiarr_collection_addition = params["notifiarr_collection_addition"] + self.notifiarr_collection_removing = params["notifiarr_collection_removing"] self.split_duplicates = params["split_duplicates"] # TODO: Here or just in Plex? self.clean_bundles = params["plex"]["clean_bundles"] # TODO: Here or just in Plex? self.empty_trash = params["plex"]["empty_trash"] # TODO: Here or just in Plex? @@ -175,6 +179,9 @@ class Library(ABC): if background_uploaded: self.config.Cache.update_image_map(item.ratingKey, f"{self.image_table_name}_backgrounds", item.art, background.compare) + def notify(self, text, collection=None, critical=True): + self.config.notify(text, library=self, collection=collection, critical=critical) + @abstractmethod def _upload_image(self, item, image): pass diff --git a/modules/meta.py b/modules/meta.py index fdca9a16..e83f6e24 100644 --- a/modules/meta.py +++ b/modules/meta.py @@ -249,6 +249,7 @@ class Metadata: add_edit("originally_available", item, meta, methods, key="originallyAvailableAt", value=originally_available, var_type="date") add_edit("critic_rating", item, meta, methods, value=rating, key="rating", var_type="float") add_edit("audience_rating", item, meta, methods, key="audienceRating", var_type="float") + add_edit("user_rating", item, meta, methods, key="userRating", var_type="float") add_edit("content_rating", item, meta, methods, key="contentRating") add_edit("original_title", item, meta, methods, key="originalTitle", value=original_title) add_edit("studio", item, meta, methods, value=studio) diff --git a/modules/notifiarr.py b/modules/notifiarr.py new file mode 100644 index 00000000..d729aa3c --- /dev/null +++ b/modules/notifiarr.py @@ -0,0 +1,76 @@ +import logging + +from modules.util import Failed + +logger = logging.getLogger("Plex Meta Manager") + +base_url = "https://notifiarr.com/api/v1/" +dev_url = "https://dev.notifiarr.com/api/v1/" + +class NotifiarrBase: + def __init__(self, config, apikey, develop, test, error_notification): + self.config = config + self.apikey = apikey + self.develop = develop + self.test = test + self.error_notification = error_notification + + def _request(self, path, json=None, params=None): + url = f"{dev_url if self.develop else base_url}" + \ + ("notification/test" if self.test else f"{path}{self.apikey}") + logger.debug(url) + response = self.config.get(url, json=json, params={"event": "pmm"} if self.test else params) + response_json = response.json() + if self.develop or self.test: + logger.debug(json) + logger.debug("") + logger.debug(response_json) + if response.status_code >= 400 or ("response" in response_json and response_json["response"] == "error"): + raise Failed(f"({response.status_code} [{response.reason}]) {response_json}") + return response_json + + def error(self, text, library=None, collection=None, critical=True): + if self.error_notification: + json = {"error": str(text), "critical": critical} + if library: + json["server_name"] = library.PlexServer.friendlyName + json["library_name"] = library.name + if collection: + json["collection"] = str(collection) + self._request("notification/plex/", json=json, params={"event": "collections"}) + +class NotifiarrFactory(NotifiarrBase): + def __init__(self, config, params): + super().__init__(config, params["apikey"], params["develop"], params["test"], params["error_notification"]) + if not params["test"] and not self._request("user/validate/")["message"]["response"]: + raise Failed("Notifiarr Error: Invalid apikey") + + def getNotifiarr(self, library): + return Notifiarr(self.config, library, self.apikey, self.develop, self.test, self.error_notification) + +class Notifiarr(NotifiarrBase): + def __init__(self, config, library, apikey, develop, test, error_notification): + super().__init__(config, apikey, develop, test, error_notification) + self.library = library + + def plex_collection(self, collection, created=False, additions=None, removals=None): + thumb = None + if collection.thumb and next((f for f in collection.fields if f.name == "thumb"), None): + thumb = self.config.get_image_encoded(f"{self.library.url}{collection.thumb}?X-Plex-Token={self.library.token}") + art = None + if collection.art and next((f for f in collection.fields if f.name == "art"), None): + art = self.config.get_image_encoded(f"{self.library.url}{collection.art}?X-Plex-Token={self.library.token}") + json = { + "server_name": self.library.PlexServer.friendlyName, + "library_name": self.library.name, + "type": "movie" if self.library.is_movie else "show", + "collection": collection.title, + "created": created, + "poster": thumb, + "background": art + } + if additions: + json["additions"] = additions + if removals: + json["removals"] = removals + self._request("notification/plex/", json=json, params={"event": "collections"}) diff --git a/modules/tmdb.py b/modules/tmdb.py index 6bc9b28d..7fee8109 100644 --- a/modules/tmdb.py +++ b/modules/tmdb.py @@ -56,9 +56,12 @@ class TMDb: self.TMDb = tmdbv3api.TMDb(session=self.config.session) self.TMDb.api_key = params["apikey"] self.TMDb.language = params["language"] - response = tmdbv3api.Configuration().info() - if hasattr(response, "status_message"): - raise Failed(f"TMDb Error: {response.status_message}") + try: + response = tmdbv3api.Configuration().info() + if hasattr(response, "status_message"): + raise Failed(f"TMDb Error: {response.status_message}") + except TMDbException as e: + raise Failed(f"TMDb Error: {e}") self.apikey = params["apikey"] self.language = params["language"] self.Movie = tmdbv3api.Movie() diff --git a/modules/util.py b/modules/util.py index 1eb6dc0b..4fab89d5 100644 --- a/modules/util.py +++ b/modules/util.py @@ -29,6 +29,9 @@ class ImageData: self.compare = location if is_url else os.stat(location).st_size self.message = f"{prefix}{'poster' if is_poster else 'background'} to [{'URL' if is_url else 'File'}] {location}" + def __str__(self): + return str(self.__dict__) + def retry_if_not_failed(exception): return not isinstance(exception, Failed) diff --git a/plex_meta_manager.py b/plex_meta_manager.py index cd33f183..091bb8eb 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -98,7 +98,7 @@ logger.addHandler(cmd_handler) sys.excepthook = util.my_except_hook -def start(config_path, is_test=False, time_scheduled=None, requested_collections=None, requested_libraries=None, resume_from=None): +def start(attrs): file_logger = os.path.join(default_dir, "logs", "meta.log") should_roll_over = os.path.isfile(file_logger) file_handler = RotatingFileHandler(file_logger, delay=True, mode="w", backupCount=10, encoding="utf-8") @@ -115,107 +115,117 @@ def start(config_path, is_test=False, time_scheduled=None, requested_collections logger.info(util.centered("| __/| | __/> < | | | | __/ || (_| | | | | | (_| | | | | (_| | (_| | __/ | ")) logger.info(util.centered("|_| |_|\\___/_/\\_\\ |_| |_|\\___|\\__\\__,_| |_| |_|\\__,_|_| |_|\\__,_|\\__, |\\___|_| ")) logger.info(util.centered(" |___/ ")) - logger.info(util.centered(" Version: 1.12.2-develop0930 ")) - if time_scheduled: start_type = f"{time_scheduled} " - elif is_test: start_type = "Test " - elif requested_collections: start_type = "Collections " - elif requested_libraries: start_type = "Libraries " + logger.info(util.centered(" Version: 1.12.2-develop1004 ")) + if "time" in attrs: start_type = f"{attrs['time']} " + elif "test" in attrs: start_type = "Test " + elif "collections" in attrs: start_type = "Collections " + elif "libraries" in attrs: start_type = "Libraries " else: start_type = "" start_time = datetime.now() - if time_scheduled is None: - time_scheduled = start_time.strftime("%H:%M") + if "time" not in attrs: + attrs["time"] = start_time.strftime("%H:%M") util.separator(f"Starting {start_type}Run") try: - config = Config(default_dir, config_path=config_path, is_test=is_test, - time_scheduled=time_scheduled, requested_collections=requested_collections, - requested_libraries=requested_libraries, resume_from=resume_from) - update_libraries(config) + config = Config(default_dir, attrs) except Exception as e: util.print_stacktrace() util.print_multiline(e, critical=True) + else: + try: + update_libraries(config) + except Exception as e: + config.notify(e) + util.print_stacktrace() + util.print_multiline(e, critical=True) logger.info("") util.separator(f"Finished {start_type}Run\nRun Time: {str(datetime.now() - start_time).split('.')[0]}") logger.removeHandler(file_handler) def update_libraries(config): for library in config.libraries: - os.makedirs(os.path.join(default_dir, "logs", library.mapping_name, "collections"), exist_ok=True) - col_file_logger = os.path.join(default_dir, "logs", library.mapping_name, "library.log") - should_roll_over = os.path.isfile(col_file_logger) - library_handler = RotatingFileHandler(col_file_logger, delay=True, mode="w", backupCount=3, encoding="utf-8") - util.apply_formatter(library_handler) - if should_roll_over: - library_handler.doRollover() - logger.addHandler(library_handler) - - os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(library.timeout) - logger.info("") - util.separator(f"{library.name} Library") - items = None - if not library.is_other: - logger.info("") - util.separator(f"Mapping {library.name} Library", space=False, border=False) - logger.info("") - items = library.map_guids() - if not config.test_mode and not config.resume_from and not collection_only and library.mass_update: - mass_metadata(config, library, items=items) - for metadata in library.metadata_files: - logger.info("") - util.separator(f"Running Metadata File\n{metadata.path}") - if not config.test_mode and not config.resume_from and not collection_only: - try: - metadata.update_metadata() - except Failed as e: - logger.error(e) - collections_to_run = metadata.get_collections(config.requested_collections) - if config.resume_from and config.resume_from not in collections_to_run: - logger.info("") - logger.warning(f"Collection: {config.resume_from} not in Metadata File: {metadata.path}") - continue - if collections_to_run and not library_only: - logger.info("") - util.separator(f"{'Test ' if config.test_mode else ''}Collections") - logger.removeHandler(library_handler) - run_collection(config, library, metadata, collections_to_run) - logger.addHandler(library_handler) - if library.run_sort: - logger.info("") - util.separator(f"Sorting {library.name} Library's Collections", space=False, border=False) + try: + os.makedirs(os.path.join(default_dir, "logs", library.mapping_name, "collections"), exist_ok=True) + col_file_logger = os.path.join(default_dir, "logs", library.mapping_name, "library.log") + should_roll_over = os.path.isfile(col_file_logger) + library_handler = RotatingFileHandler(col_file_logger, delay=True, mode="w", backupCount=3, encoding="utf-8") + util.apply_formatter(library_handler) + if should_roll_over: + library_handler.doRollover() + logger.addHandler(library_handler) + + os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(library.timeout) logger.info("") - for builder in library.run_sort: + util.separator(f"{library.name} Library") + items = None + if not library.is_other: logger.info("") - util.separator(f"Sorting {builder.name} Collection", space=False, border=False) + util.separator(f"Mapping {library.name} Library", space=False, border=False) logger.info("") - builder.sort_collection() - - if not config.test_mode and not config.requested_collections and ((library.show_unmanaged and not library_only) or (library.assets_for_all and not collection_only)): - logger.info("") - util.separator(f"Other {library.name} Library Operations") - unmanaged_collections = [] - for col in library.get_all_collections(): - if col.title not in library.collections: - unmanaged_collections.append(col) - - if library.show_unmanaged and not library_only: + items = library.map_guids() + if not config.test_mode and not config.resume_from and not collection_only and library.mass_update: + mass_metadata(config, library, items=items) + for metadata in library.metadata_files: logger.info("") - util.separator(f"Unmanaged Collections in {library.name} Library", space=False, border=False) + util.separator(f"Running Metadata File\n{metadata.path}") + if not config.test_mode and not config.resume_from and not collection_only: + try: + metadata.update_metadata() + except Failed as e: + library.notify(e) + logger.error(e) + collections_to_run = metadata.get_collections(config.requested_collections) + if config.resume_from and config.resume_from not in collections_to_run: + logger.info("") + logger.warning(f"Collection: {config.resume_from} not in Metadata File: {metadata.path}") + continue + if collections_to_run and not library_only: + logger.info("") + util.separator(f"{'Test ' if config.test_mode else ''}Collections") + logger.removeHandler(library_handler) + run_collection(config, library, metadata, collections_to_run) + logger.addHandler(library_handler) + if library.run_sort: logger.info("") - for col in unmanaged_collections: - logger.info(col.title) + util.separator(f"Sorting {library.name} Library's Collections", space=False, border=False) logger.info("") - logger.info(f"{len(unmanaged_collections)} Unmanaged Collections") + for builder in library.run_sort: + logger.info("") + util.separator(f"Sorting {builder.name} Collection", space=False, border=False) + logger.info("") + builder.sort_collection() - if library.assets_for_all and not collection_only: - logger.info("") - util.separator(f"All {library.type}s Assets Check for {library.name} Library", space=False, border=False) + if not config.test_mode and not config.requested_collections and ((library.show_unmanaged and not library_only) or (library.assets_for_all and not collection_only)): logger.info("") - for col in unmanaged_collections: - poster, background = library.find_collection_assets(col, create=library.create_asset_folders) - library.upload_images(col, poster=poster, background=background) - for item in library.get_all(): - library.update_item_from_assets(item, create=library.create_asset_folders) + util.separator(f"Other {library.name} Library Operations") + unmanaged_collections = [] + for col in library.get_all_collections(): + if col.title not in library.collections: + unmanaged_collections.append(col) + + if library.show_unmanaged and not library_only: + logger.info("") + util.separator(f"Unmanaged Collections in {library.name} Library", space=False, border=False) + logger.info("") + for col in unmanaged_collections: + logger.info(col.title) + logger.info("") + logger.info(f"{len(unmanaged_collections)} Unmanaged Collections") - logger.removeHandler(library_handler) + if library.assets_for_all and not collection_only: + logger.info("") + util.separator(f"All {library.type}s Assets Check for {library.name} Library", space=False, border=False) + logger.info("") + for col in unmanaged_collections: + poster, background = library.find_collection_assets(col, create=library.create_asset_folders) + library.upload_images(col, poster=poster, background=background) + for item in library.get_all(): + library.update_item_from_assets(item, create=library.create_asset_folders) + + logger.removeHandler(library_handler) + except Exception as e: + library.notify(e) + util.print_stacktrace() + util.print_multiline(e, critical=True) has_run_again = False for library in config.libraries: @@ -234,26 +244,32 @@ def update_libraries(config): util.print_end() for library in config.libraries: if library.run_again: - col_file_logger = os.path.join(default_dir, "logs", library.mapping_name, f"library.log") - library_handler = RotatingFileHandler(col_file_logger, mode="w", backupCount=3, encoding="utf-8") - util.apply_formatter(library_handler) - logger.addHandler(library_handler) - library_handler.addFilter(fmt_filter) - os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(library.timeout) - logger.info("") - util.separator(f"{library.name} Library Run Again") - logger.info("") - library.map_guids() - for builder in library.run_again: + try: + col_file_logger = os.path.join(default_dir, "logs", library.mapping_name, f"library.log") + library_handler = RotatingFileHandler(col_file_logger, mode="w", backupCount=3, encoding="utf-8") + util.apply_formatter(library_handler) + logger.addHandler(library_handler) + library_handler.addFilter(fmt_filter) + os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(library.timeout) logger.info("") - util.separator(f"{builder.name} Collection") + util.separator(f"{library.name} Library Run Again") logger.info("") - try: - builder.run_collections_again() - except Failed as e: - util.print_stacktrace() - util.print_multiline(e, error=True) - logger.removeHandler(library_handler) + library.map_guids() + for builder in library.run_again: + logger.info("") + util.separator(f"{builder.name} Collection") + logger.info("") + try: + builder.run_collections_again() + except Failed as e: + library.notify(e, collection=builder.name, critical=False) + util.print_stacktrace() + util.print_multiline(e, error=True) + logger.removeHandler(library_handler) + except Exception as e: + library.notify(e) + util.print_stacktrace() + util.print_multiline(e, critical=True) used_url = [] for library in config.libraries: @@ -457,7 +473,7 @@ def run_collection(config, library, metadata, requested_collections): 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, f"collection.log") + 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) @@ -533,6 +549,8 @@ def run_collection(config, library, metadata, requested_collections): library.run_sort.append(builder) # builder.sort_collection() + builder.send_notifications() + if builder.item_details and run_item_details: try: builder.load_collection_items() @@ -546,9 +564,11 @@ def run_collection(config, library, metadata, requested_collections): library.run_again.append(builder) 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("") @@ -557,7 +577,13 @@ def run_collection(config, library, metadata, requested_collections): try: if run or test or collections or libraries or resume: - start(config_file, is_test=test, requested_collections=collections, requested_libraries=libraries, resume_from=resume) + start({ + "config_file": config_file, + "test": test, + "collections": collections, + "libraries": libraries, + "resume": resume + }) else: times_to_run = util.get_list(times) valid_times = [] @@ -570,7 +596,7 @@ try: 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, time_scheduled=time_to_run) + schedule.every().day.at(time_to_run).do(start, {"config_file": config_file, "time": time_to_run}) while True: schedule.run_pending() if not no_countdown: