diff --git a/modules/builder.py b/modules/builder.py index 214cc0f4..95ce325a 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -86,7 +86,7 @@ scheduled_boolean = ["visible_library", "visible_home", "visible_shared"] string_details = ["sort_title", "content_rating", "name_mapping"] ignored_details = [ "smart_filter", "smart_label", "smart_url", "run_again", "schedule", "sync_mode", "template", "test", "delete_not_scheduled", - "tmdb_person", "build_collection", "collection_order", "collection_level", "validate_builders", "collection_name", "sort_by", "libraries" + "tmdb_person", "build_collection", "collection_order", "collection_level", "validate_builders", "collection_name", "sort_by", "libraries", "sync_to_users" ] details = ["ignore_ids", "ignore_imdb_ids", "server_preroll", "collection_changes_webhooks", "collection_mode", "collection_minimum", "label"] + boolean_details + scheduled_boolean + string_details @@ -2136,6 +2136,37 @@ class CollectionBuilder: self.library.moveItem(self.obj, item, previous) previous = item + def delete_user_playlist(self, title, user): + user_server = self.library.PlexServer.switchUser(user) + user_playlist = user_server.playlist(title) + user_playlist.delete() + + def delete_playlist(self, users): + if self.obj: + self.library.query(self.obj.delete) + logger.info("") + logger.info(f"Playlist {self.obj.title} deleted") + if users: + for user in users: + try: + self.delete_user_playlist(self.obj.title, user) + logger.info(f"Playlist {self.obj.title} deleted on User {user}") + except NotFound: + logger.error(f"Playlist {self.obj.title} not found on User {user}") + + def sync_playlist(self, users): + if self.obj and users: + logger.info("") + util.separator(f"Syncing Playlist to Users", space=False, border=False) + logger.info("") + for user in users: + try: + self.delete_user_playlist(self.obj.title, user) + except NotFound: + pass + self.obj.copyToUser(user) + logger.info(f"Playlist: {self.name} synced to {user}") + def send_notifications(self, playlist=False): if self.obj and self.details["collection_changes_webhooks"] and \ (self.created or len(self.notification_additions) > 0 or len(self.notification_removals) > 0): diff --git a/modules/config.py b/modules/config.py index 5dd42e1b..7f7f7840 100644 --- a/modules/config.py +++ b/modules/config.py @@ -241,6 +241,7 @@ class ConfigFile: "tvdb_language": check_for_attribute(self.data, "tvdb_language", parent="settings", default="default"), "ignore_ids": check_for_attribute(self.data, "ignore_ids", parent="settings", var_type="int_list", default_is_none=True), "ignore_imdb_ids": check_for_attribute(self.data, "ignore_imdb_ids", parent="settings", var_type="list", default_is_none=True), + "playlist_sync_to_user": check_for_attribute(self.data, "playlist_sync_to_user", parent="settings", default="all"), "assets_for_all": check_for_attribute(self.data, "assets_for_all", parent="settings", var_type="bool", default=False, save=False, do_print=False) } self.webhooks = { diff --git a/modules/plex.py b/modules/plex.py index c9dc1150..0de6700e 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -252,6 +252,7 @@ class Plex(Library): else: raise Failed(f"Plex Error: Plex Library must be a Movies or TV Shows library") + self._users = [] self.agent = self.Plex.agent self.is_movie = self.type == "Movie" self.is_show = self.type == "Show" @@ -412,6 +413,16 @@ class Plex(Library): else: method = None return self.Plex._server.query(key, method=method) + @property + def users(self): + if not self._users: + users = [] + for user in self.PlexServer.myPlexAccount().users(): + if self.PlexServer.machineIdentifier in [s.machineIdentifier for s in user.servers]: + users.append(user.title) + self._users = users + return self._users + def alter_collection(self, item, collection, smart_label_collection=False, add=True): if smart_label_collection: self.query_data(item.addLabel if add else item.removeLabel, collection) diff --git a/modules/webhooks.py b/modules/webhooks.py index 5e10ae70..6bf56e28 100644 --- a/modules/webhooks.py +++ b/modules/webhooks.py @@ -66,14 +66,10 @@ class Webhooks: def error_hooks(self, text, server=None, library=None, collection=None, playlist=None, critical=True): if self.error_webhooks: json = {"error": str(text), "critical": critical} - if server: - json["server_name"] = str(server) - if library: - json["library_name"] = str(library) - if collection: - json["collection"] = str(collection) - if playlist: - json["playlist"] = str(playlist) + if server: json["server_name"] = str(server) + if library: json["library_name"] = str(library) + if collection: json["collection"] = str(collection) + if playlist: json["playlist"] = str(playlist) self._request(self.error_webhooks, json) def collection_hooks(self, webhooks, collection, poster_url=None, background_url=None, created=False, deleted=False, additions=None, removals=None, playlist=False): diff --git a/plex_meta_manager.py b/plex_meta_manager.py index ed3bc92a..04beac07 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -287,10 +287,7 @@ def update_libraries(config): util.print_stacktrace() util.print_multiline(e, critical=True) - - if config.playlist_files: - library_map = {_l.original_mapping_name: _l for _l in config.libraries} os.makedirs(os.path.join(default_dir, "logs", "playlists"), exist_ok=True) pf_file_logger = os.path.join(default_dir, "logs", "playlists", "playlists.log") should_roll_over = os.path.isfile(pf_file_logger) @@ -299,292 +296,7 @@ def update_libraries(config): if should_roll_over: playlists_handler.doRollover() logger.addHandler(playlists_handler) - - logger.info("") - util.separator("Playlists") - logger.info("") - for playlist_file in config.playlist_files: - for mapping_name, playlist_attrs in playlist_file.playlists.items(): - playlist_start = datetime.now() - if config.test_mode and ("test" not in playlist_attrs or playlist_attrs["test"] is not True): - no_template_test = True - if "template" in playlist_attrs and playlist_attrs["template"]: - for data_template in util.get_list(playlist_attrs["template"], split=False): - if "name" in data_template \ - and data_template["name"] \ - and playlist_file.templates \ - and data_template["name"] in playlist_file.templates \ - and playlist_file.templates[data_template["name"]] \ - and "test" in playlist_file.templates[data_template["name"]] \ - and playlist_file.templates[data_template["name"]]["test"] is True: - no_template_test = False - if no_template_test: - continue - - if "name_mapping" in playlist_attrs and playlist_attrs["name_mapping"]: - playlist_log_name, output_str = util.validate_filename(playlist_attrs["name_mapping"]) - else: - playlist_log_name, output_str = util.validate_filename(mapping_name) - playlist_log_folder = os.path.join(default_dir, "logs", "playlists", playlist_log_name) - os.makedirs(playlist_log_folder, exist_ok=True) - ply_file_logger = os.path.join(playlist_log_folder, "playlist.log") - should_roll_over = os.path.isfile(ply_file_logger) - playlist_handler = RotatingFileHandler(ply_file_logger, delay=True, mode="w", backupCount=3, encoding="utf-8") - util.apply_formatter(playlist_handler) - if should_roll_over: - playlist_handler.doRollover() - logger.addHandler(playlist_handler) - server_name = None - library_names = None - try: - util.separator(f"{mapping_name} Playlist") - logger.info("") - if output_str: - logger.info(output_str) - logger.info("") - if "libraries" not in playlist_attrs or not playlist_attrs["libraries"]: - raise Failed("Playlist Error: libraries attribute is required and cannot be blank") - pl_libraries = [] - for pl_library in util.get_list(playlist_attrs["libraries"]): - if str(pl_library) in library_map: - pl_libraries.append(library_map[pl_library]) - else: - raise Failed(f"Playlist Error: Library: {pl_library} not defined") - server_check = None - for pl_library in pl_libraries: - if server_check: - if pl_library.PlexServer.machineIdentifier != server_check: - raise Failed("Playlist Error: All defined libraries must be on the same server") - else: - server_check = pl_library.PlexServer.machineIdentifier - - util.separator(f"Validating {mapping_name} Attributes", space=False, border=False) - - builder = CollectionBuilder(config, pl_libraries[0], playlist_file, mapping_name, no_missing, playlist_attrs, playlist=True) - logger.info("") - - util.separator(f"Running {mapping_name} Playlist", space=False, border=False) - - if len(builder.schedule) > 0: - util.print_multiline(builder.schedule, info=True) - - items_added = 0 - items_removed = 0 - logger.info("") - logger.info(f"Sync Mode: {'sync' if builder.sync else 'append'}") - - if builder.filters or builder.tmdb_filters: - logger.info("") - for filter_key, filter_value in builder.filters: - logger.info(f"Playlist Filter {filter_key}: {filter_value}") - for filter_key, filter_value in builder.tmdb_filters: - logger.info(f"Playlist Filter {filter_key}: {filter_value}") - - method, value = builder.builders[0] - logger.debug("") - logger.debug(f"Builder: {method}: {value}") - logger.info("") - items = [] - ids = builder.gather_ids(method, value) - - if len(ids) > 0: - total_ids = len(ids) - logger.debug("") - logger.debug(f"{total_ids} IDs Found: {ids}") - for i, input_data in enumerate(ids, 1): - input_id, id_type = input_data - util.print_return(f"Parsing ID {i}/{total_ids}") - if id_type == "tvdb_season": - show_id, season_num = input_id.split("_") - show_id = int(show_id) - found = False - for pl_library in pl_libraries: - if show_id in pl_library.show_map: - found = True - show_item = pl_library.fetchItem(pl_library.show_map[show_id][0]) - try: - items.extend(show_item.season(season=int(season_num)).episodes()) - except NotFound: - builder.missing_parts.append(f"{show_item.title} Season: {season_num} Missing") - break - if not found and show_id not in builder.missing_shows: - builder.missing_shows.append(show_id) - elif id_type == "tvdb_episode": - show_id, season_num, episode_num = input_id.split("_") - show_id = int(show_id) - found = False - for pl_library in pl_libraries: - if show_id in pl_library.show_map: - found = True - show_item = pl_library.fetchItem(pl_library.show_map[show_id][0]) - try: - items.append(show_item.episode(season=int(season_num), episode=int(episode_num))) - except NotFound: - builder.missing_parts.append(f"{show_item.title} Season: {season_num} Episode: {episode_num} Missing") - break - if not found and show_id not in builder.missing_shows: - builder.missing_shows.append(show_id) - else: - rating_keys = [] - if id_type == "ratingKey": - rating_keys = input_id - elif id_type == "tmdb": - if input_id not in builder.ignore_ids: - found = False - for pl_library in pl_libraries: - if input_id in pl_library.movie_map: - found = True - rating_keys = pl_library.movie_map[input_id] - break - if not found and input_id not in builder.missing_movies: - builder.missing_movies.append(input_id) - elif id_type in ["tvdb", "tmdb_show"]: - if id_type == "tmdb_show": - try: - input_id = config.Convert.tmdb_to_tvdb(input_id, fail=True) - except Failed as e: - logger.error(e) - continue - if input_id not in builder.ignore_ids: - found = False - for pl_library in pl_libraries: - if input_id in pl_library.show_map: - found = True - rating_keys = pl_library.show_map[input_id] - break - if not found and input_id not in builder.missing_shows: - builder.missing_shows.append(input_id) - elif id_type == "imdb": - if input_id not in builder.ignore_imdb_ids: - found = False - for pl_library in pl_libraries: - if input_id in pl_library.imdb_map: - found = True - rating_keys = pl_library.imdb_map[input_id] - break - if not found: - try: - _id, tmdb_type = config.Convert.imdb_to_tmdb(input_id, fail=True) - if tmdb_type == "episode": - tmdb_id, season_num, episode_num = _id.split("_") - show_id = config.Convert.tmdb_to_tvdb(tmdb_id, fail=True) - show_id = int(show_id) - found = False - for pl_library in pl_libraries: - if show_id in pl_library.show_map: - found = True - show_item = pl_library.fetchItem(pl_library.show_map[show_id][0]) - try: - items.append(show_item.episode(season=int(season_num), episode=int(episode_num))) - except NotFound: - builder.missing_parts.append(f"{show_item.title} Season: {season_num} Episode: {episode_num} Missing") - break - if not found and show_id not in builder.missing_shows: - builder.missing_shows.append(show_id) - elif tmdb_type == "movie" and builder.do_missing: - if _id not in builder.missing_movies: - builder.missing_movies.append(_id) - elif tmdb_type == "show" and builder.do_missing: - tvdb_id = config.Convert.tmdb_to_tvdb(_id, fail=True) - if tvdb_id not in builder.missing_shows: - builder.missing_shows.append(tvdb_id) - except Failed as e: - logger.error(e) - continue - if not isinstance(rating_keys, list): - rating_keys = [rating_keys] - for rk in rating_keys: - try: - item = builder.fetch_item(rk) - if isinstance(item, (Show, Season)): - items.extend(item.episodes()) - else: - items.append(item) - except Failed as e: - logger.error(e) - util.print_end() - - if len(items) > 0: - builder.filter_and_save_items(items) - - if len(builder.added_items) >= builder.minimum: - logger.info("") - util.separator(f"Adding to {mapping_name} Playlist", space=False, border=False) - logger.info("") - items_added = builder.add_to_collection() - stats["added"] += items_added - items_removed = 0 - if builder.sync: - items_removed = builder.sync_collection() - stats["removed"] += items_removed - elif len(builder.added_items) < builder.minimum: - logger.info("") - logger.info(f"Playlist Minimum: {builder.minimum} not met for {mapping_name} Playlist") - if builder.details["delete_below_minimum"] and builder.obj: - builder.delete_collection() - builder.deleted = True - logger.info("") - logger.info(f"Playlist {builder.obj.title} deleted") - - if builder.do_missing and (len(builder.missing_movies) > 0 or len(builder.missing_shows) > 0): - if builder.details["show_missing"] is True: - logger.info("") - util.separator(f"Missing from Library", space=False, border=False) - logger.info("") - radarr_add, sonarr_add = builder.run_missing() - stats["radarr"] += radarr_add - stats["sonarr"] += sonarr_add - - run_item_details = True - try: - builder.load_collection() - if builder.created: - stats["created"] += 1 - elif items_added > 0 or items_removed > 0: - stats["modified"] += 1 - except Failed: - util.print_stacktrace() - run_item_details = False - logger.info("") - util.separator("No Playlist to Update", space=False, border=False) - else: - builder.update_details() - - if builder.deleted: - stats["deleted"] += 1 - - if (builder.item_details or builder.custom_sort) and run_item_details and builder.builders: - try: - builder.load_collection_items() - except Failed: - logger.info("") - util.separator("No Items Found", space=False, border=False) - else: - if builder.item_details: - builder.update_item_details() - if builder.custom_sort: - builder.sort_collection() - - builder.send_notifications(playlist=True) - - - - - - - except NotScheduled as e: - util.print_multiline(e, info=True) - except Failed as e: - config.notify(e, server=server_name, library=library_names, playlist=mapping_name) - util.print_stacktrace() - util.print_multiline(e, error=True) - except Exception as e: - config.notify(f"Unknown Error: {e}", server=server_name, library=library_names, playlist=mapping_name) - util.print_stacktrace() - logger.error(f"Unknown Error: {e}") - logger.info("") - util.separator(f"Finished {mapping_name} Playlist\nPlaylist Run Time: {str(datetime.now() - playlist_start).split('.')[0]}") - logger.removeHandler(playlist_handler) + run_playlists(config) logger.removeHandler(playlists_handler) has_run_again = False @@ -1077,6 +789,320 @@ def run_collection(config, library, metadata, requested_collections): util.separator(f"Finished {mapping_name} Collection\nCollection Run Time: {str(datetime.now() - collection_start).split('.')[0]}") logger.removeHandler(collection_handler) +def run_playlists(config): + logger.info("") + util.separator("Playlists") + logger.info("") + library_map = {_l.original_mapping_name: _l for _l in config.libraries} + for playlist_file in config.playlist_files: + for mapping_name, playlist_attrs in playlist_file.playlists.items(): + playlist_start = datetime.now() + if config.test_mode and ("test" not in playlist_attrs or playlist_attrs["test"] is not True): + no_template_test = True + if "template" in playlist_attrs and playlist_attrs["template"]: + for data_template in util.get_list(playlist_attrs["template"], split=False): + if "name" in data_template \ + and data_template["name"] \ + and playlist_file.templates \ + and data_template["name"] in playlist_file.templates \ + and playlist_file.templates[data_template["name"]] \ + and "test" in playlist_file.templates[data_template["name"]] \ + and playlist_file.templates[data_template["name"]]["test"] is True: + no_template_test = False + if no_template_test: + continue + + if "name_mapping" in playlist_attrs and playlist_attrs["name_mapping"]: + playlist_log_name, output_str = util.validate_filename(playlist_attrs["name_mapping"]) + else: + playlist_log_name, output_str = util.validate_filename(mapping_name) + playlist_log_folder = os.path.join(default_dir, "logs", "playlists", playlist_log_name) + os.makedirs(playlist_log_folder, exist_ok=True) + ply_file_logger = os.path.join(playlist_log_folder, "playlist.log") + should_roll_over = os.path.isfile(ply_file_logger) + playlist_handler = RotatingFileHandler(ply_file_logger, delay=True, mode="w", backupCount=3, + encoding="utf-8") + util.apply_formatter(playlist_handler) + if should_roll_over: + playlist_handler.doRollover() + logger.addHandler(playlist_handler) + server_name = None + library_names = None + try: + util.separator(f"{mapping_name} Playlist") + logger.info("") + if output_str: + logger.info(output_str) + logger.info("") + if "libraries" not in playlist_attrs or not playlist_attrs["libraries"]: + raise Failed("Playlist Error: libraries attribute is required and cannot be blank") + pl_libraries = [] + for pl_library in util.get_list(playlist_attrs["libraries"]): + if str(pl_library) in library_map: + pl_libraries.append(library_map[pl_library]) + else: + raise Failed(f"Playlist Error: Library: {pl_library} not defined") + server_check = None + for pl_library in pl_libraries: + if server_check: + if pl_library.PlexServer.machineIdentifier != server_check: + raise Failed("Playlist Error: All defined libraries must be on the same server") + else: + server_check = pl_library.PlexServer.machineIdentifier + + sync_to_users = config.general["playlist_sync_to_user"] + if "sync_to_users" not in playlist_attrs: + logger.warning(f"Playlist Error: sync_to_users attribute not found defaulting to playlist_sync_to_user: {sync_to_users}") + elif not playlist_attrs["sync_to_users"]: + logger.warning(f"Playlist Error: sync_to_users attribute is blank defaulting to playlist_sync_to_user: {sync_to_users}") + else: + sync_to_users = playlist_attrs["sync_to_users"] + + valid_users = [] + plex_users = pl_libraries[0].users + if str(sync_to_users) == "all": + valid_users = plex_users + else: + for user in util.get_list(sync_to_users): + if user in plex_users: + valid_users.append(user) + else: + raise Failed(f"Playlist Error: User: {user} not found in plex\nOptions: {plex_users}") + + util.separator(f"Validating {mapping_name} Attributes", space=False, border=False) + + builder = CollectionBuilder(config, pl_libraries[0], playlist_file, mapping_name, no_missing, + playlist_attrs, playlist=True) + logger.info("") + + util.separator(f"Running {mapping_name} Playlist", space=False, border=False) + + if len(builder.schedule) > 0: + util.print_multiline(builder.schedule, info=True) + + items_added = 0 + items_removed = 0 + valid = True + logger.info("") + logger.info(f"Sync Mode: {'sync' if builder.sync else 'append'}") + + if builder.filters or builder.tmdb_filters: + logger.info("") + for filter_key, filter_value in builder.filters: + logger.info(f"Playlist Filter {filter_key}: {filter_value}") + for filter_key, filter_value in builder.tmdb_filters: + logger.info(f"Playlist Filter {filter_key}: {filter_value}") + + method, value = builder.builders[0] + logger.debug("") + logger.debug(f"Builder: {method}: {value}") + logger.info("") + items = [] + ids = builder.gather_ids(method, value) + + if len(ids) > 0: + total_ids = len(ids) + logger.debug("") + logger.debug(f"{total_ids} IDs Found: {ids}") + for i, input_data in enumerate(ids, 1): + input_id, id_type = input_data + util.print_return(f"Parsing ID {i}/{total_ids}") + if id_type == "tvdb_season": + show_id, season_num = input_id.split("_") + show_id = int(show_id) + found = False + for pl_library in pl_libraries: + if show_id in pl_library.show_map: + found = True + show_item = pl_library.fetchItem(pl_library.show_map[show_id][0]) + try: + items.extend(show_item.season(season=int(season_num)).episodes()) + except NotFound: + builder.missing_parts.append(f"{show_item.title} Season: {season_num} Missing") + break + if not found and show_id not in builder.missing_shows: + builder.missing_shows.append(show_id) + elif id_type == "tvdb_episode": + show_id, season_num, episode_num = input_id.split("_") + show_id = int(show_id) + found = False + for pl_library in pl_libraries: + if show_id in pl_library.show_map: + found = True + show_item = pl_library.fetchItem(pl_library.show_map[show_id][0]) + try: + items.append( + show_item.episode(season=int(season_num), episode=int(episode_num))) + except NotFound: + builder.missing_parts.append( + f"{show_item.title} Season: {season_num} Episode: {episode_num} Missing") + break + if not found and show_id not in builder.missing_shows: + builder.missing_shows.append(show_id) + else: + rating_keys = [] + if id_type == "ratingKey": + rating_keys = input_id + elif id_type == "tmdb": + if input_id not in builder.ignore_ids: + found = False + for pl_library in pl_libraries: + if input_id in pl_library.movie_map: + found = True + rating_keys = pl_library.movie_map[input_id] + break + if not found and input_id not in builder.missing_movies: + builder.missing_movies.append(input_id) + elif id_type in ["tvdb", "tmdb_show"]: + if id_type == "tmdb_show": + try: + input_id = config.Convert.tmdb_to_tvdb(input_id, fail=True) + except Failed as e: + logger.error(e) + continue + if input_id not in builder.ignore_ids: + found = False + for pl_library in pl_libraries: + if input_id in pl_library.show_map: + found = True + rating_keys = pl_library.show_map[input_id] + break + if not found and input_id not in builder.missing_shows: + builder.missing_shows.append(input_id) + elif id_type == "imdb": + if input_id not in builder.ignore_imdb_ids: + found = False + for pl_library in pl_libraries: + if input_id in pl_library.imdb_map: + found = True + rating_keys = pl_library.imdb_map[input_id] + break + if not found: + try: + _id, tmdb_type = config.Convert.imdb_to_tmdb(input_id, fail=True) + if tmdb_type == "episode": + tmdb_id, season_num, episode_num = _id.split("_") + show_id = config.Convert.tmdb_to_tvdb(tmdb_id, fail=True) + show_id = int(show_id) + found = False + for pl_library in pl_libraries: + if show_id in pl_library.show_map: + found = True + show_item = pl_library.fetchItem( + pl_library.show_map[show_id][0]) + try: + items.append(show_item.episode(season=int(season_num), + episode=int(episode_num))) + except NotFound: + builder.missing_parts.append( + f"{show_item.title} Season: {season_num} Episode: {episode_num} Missing") + break + if not found and show_id not in builder.missing_shows: + builder.missing_shows.append(show_id) + elif tmdb_type == "movie" and builder.do_missing: + if _id not in builder.missing_movies: + builder.missing_movies.append(_id) + elif tmdb_type == "show" and builder.do_missing: + tvdb_id = config.Convert.tmdb_to_tvdb(_id, fail=True) + if tvdb_id not in builder.missing_shows: + builder.missing_shows.append(tvdb_id) + except Failed as e: + logger.error(e) + continue + if not isinstance(rating_keys, list): + rating_keys = [rating_keys] + for rk in rating_keys: + try: + item = builder.fetch_item(rk) + if isinstance(item, (Show, Season)): + items.extend(item.episodes()) + else: + items.append(item) + except Failed as e: + logger.error(e) + util.print_end() + + if len(items) > 0: + builder.filter_and_save_items(items) + + if len(builder.added_items) >= builder.minimum: + logger.info("") + util.separator(f"Adding to {mapping_name} Playlist", space=False, border=False) + logger.info("") + items_added = builder.add_to_collection() + stats["added"] += items_added + items_removed = 0 + if builder.sync: + items_removed = builder.sync_collection() + stats["removed"] += items_removed + elif len(builder.added_items) < builder.minimum: + logger.info("") + logger.info(f"Playlist Minimum: {builder.minimum} not met for {mapping_name} Playlist") + valid = False + if builder.details["delete_below_minimum"] and builder.obj: + builder.delete_playlist(valid_users) + builder.deleted = True + + if builder.do_missing and (len(builder.missing_movies) > 0 or len(builder.missing_shows) > 0): + if builder.details["show_missing"] is True: + logger.info("") + util.separator(f"Missing from Library", space=False, border=False) + logger.info("") + radarr_add, sonarr_add = builder.run_missing() + stats["radarr"] += radarr_add + stats["sonarr"] += sonarr_add + + run_item_details = True + try: + builder.load_collection() + if builder.created: + stats["created"] += 1 + elif items_added > 0 or items_removed > 0: + stats["modified"] += 1 + except Failed: + util.print_stacktrace() + run_item_details = False + logger.info("") + util.separator("No Playlist to Update", space=False, border=False) + else: + builder.update_details() + + if builder.deleted: + stats["deleted"] += 1 + + if valid and run_item_details and builder.builders and (builder.item_details or builder.custom_sort): + try: + builder.load_collection_items() + except Failed: + logger.info("") + util.separator("No Items Found", space=False, border=False) + else: + if builder.item_details: + builder.update_item_details() + if builder.custom_sort: + builder.sort_collection() + + if valid: + builder.sync_playlist(valid_users) + + builder.send_notifications(playlist=True) + + except NotScheduled as e: + util.print_multiline(e, info=True) + except Failed as e: + config.notify(e, server=server_name, library=library_names, playlist=mapping_name) + util.print_stacktrace() + util.print_multiline(e, error=True) + except Exception as e: + config.notify(f"Unknown Error: {e}", server=server_name, library=library_names, playlist=mapping_name) + util.print_stacktrace() + logger.error(f"Unknown Error: {e}") + logger.info("") + util.separator( + f"Finished {mapping_name} Playlist\nPlaylist Run Time: {str(datetime.now() - playlist_start).split('.')[0]}") + logger.removeHandler(playlist_handler) + + try: if run or test or collections or libraries or resume: start({