diff --git a/config/config.yml.template b/config/config.yml.template index beea6805..bfec47e6 100644 --- a/config/config.yml.template +++ b/config/config.yml.template @@ -16,6 +16,7 @@ settings: # Can be individually specified show_filtered: false show_missing: true save_missing: true + run_again_delay: 2 plex: # Can be individually specified per library as well url: http://192.168.1.12:32400 token: #################### diff --git a/modules/builder.py b/modules/builder.py index a6f3c65f..73d28e7d 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -2,6 +2,8 @@ import glob, logging, os, re from datetime import datetime, timedelta from modules import util from modules.util import Failed +from plexapi.collection import Collections +from plexapi.exceptions import BadRequest, NotFound logger = logging.getLogger("Plex Meta Manager") @@ -17,12 +19,15 @@ class CollectionBuilder: "show_missing": library.show_missing, "save_missing": library.save_missing } + self.missing_movies = [] + self.missing_shows = [] self.methods = [] self.filters = [] self.posters = {} self.backgrounds = {} self.summaries = {} self.schedule = "" + self.rating_key_map = {} current_time = datetime.now() current_year = current_time.year @@ -168,12 +173,7 @@ class CollectionBuilder: logger.info(f"Scanning {self.name} Collection") self.collectionless = "plex_collectionless" in data - - self.sync = self.library.sync_mode == "sync" - if "sync_mode" in data: - if not data["sync_mode"]: logger.warning(f"Collection Warning: sync_mode attribute is blank using general: {self.library.sync_mode}") - elif data["sync_mode"] not in ["append", "sync"]: logger.warning(f"Collection Warning: {self.library.sync_mode} sync_mode invalid using general: {data['sync_mode']}") - else: self.sync = data["sync_mode"] == "sync" + self.run_again = "run_again" in data if "tmdb_person" in data: if data["tmdb_person"]: @@ -252,6 +252,9 @@ class CollectionBuilder: elif method_name == "label_sync_mode": if data[m] in ["append", "sync"]: self.details[method_name] = data[m] else: raise Failed("Collection Error: label_sync_mode attribute must be either 'append' or 'sync'") + elif method_name == "sync_mode": + if data[m] in ["append", "sync"]: self.details[method_name] = data[m] + else: raise Failed("Collection Error: sync_mode attribute must be either 'append' or 'sync'") elif method_name in ["arr_tag", "label"]: self.details[method_name] = util.get_list(data[m]) elif method_name in util.boolean_details: @@ -524,6 +527,12 @@ class CollectionBuilder: else: raise Failed(f"Collection Error: {m} attribute is blank") + self.sync = self.library.sync_mode == "sync" + if "sync_mode" in data: + if not data["sync_mode"]: logger.warning(f"Collection Warning: sync_mode attribute is blank using general: {self.library.sync_mode}") + elif data["sync_mode"] not in ["append", "sync"]: logger.warning(f"Collection Warning: {self.library.sync_mode} sync_mode invalid using general: {data['sync_mode']}") + else: self.sync = data["sync_mode"] == "sync" + self.do_arr = False if self.library.Radarr: self.do_arr = self.details["add_to_arr"] if "add_to_arr" in self.details else self.library.Radarr.add @@ -654,6 +663,8 @@ class CollectionBuilder: self.library.add_missing(collection_name, missing_movies_with_names, True) if self.do_arr and self.library.Radarr: self.library.Radarr.add_tmdb([missing_id for title, missing_id in missing_movies_with_names], tag=self.details["arr_tag"]) + if self.run_again: + self.missing_movies.extend([missing_id for title, missing_id in missing_movies_with_names]) if len(missing_shows) > 0 and self.library.is_show: missing_shows_with_names = [] for missing_id in missing_shows: @@ -675,12 +686,14 @@ class CollectionBuilder: if self.details["show_missing"] is True: logger.info(f"{collection_name} Collection | ? | {title} (TVDB: {missing_id})") elif self.details["show_filtered"] is True: - logger.info(f"{collection_name} Collection | X | {title} (TMDb: {missing_id})") + logger.info(f"{collection_name} Collection | X | {title} (TVDb: {missing_id})") logger.info(f"{len(missing_shows_with_names)} Show{'s' if len(missing_shows_with_names) > 1 else ''} Missing") if self.details["save_missing"] is True: self.library.add_missing(collection_name, missing_shows_with_names, False) if self.do_arr and self.library.Sonarr: self.library.Sonarr.add_tvdb([missing_id for title, missing_id in missing_shows_with_names], tag=self.details["arr_tag"]) + if self.run_again: + self.missing_shows.extend([missing_id for title, missing_id in missing_shows_with_names]) if self.sync and items_found > 0: logger.info("") @@ -786,12 +799,12 @@ class CollectionBuilder: def set_image(image_method, images, is_background=False): if image_method in ['file_poster', 'asset_directory']: - if is_background: collection.uploadArt(url=images[image_method]) - else: collection.uploadPoster(url=images[image_method]) - image_location = "File" - else: if is_background: collection.uploadArt(filepath=images[image_method]) else: collection.uploadPoster(filepath=images[image_method]) + image_location = "File" + else: + if is_background: collection.uploadArt(url=images[image_method]) + else: collection.uploadPoster(url=images[image_method]) image_location = "URL" logger.info(f"Detail: {image_method} updated collection {'background' if is_background else 'poster'} to [{image_location}] {images[image_method]}") @@ -825,4 +838,52 @@ class CollectionBuilder: elif "tmdb_collection_details" in self.backgrounds: set_image("tmdb_collection", self.backgrounds, is_background=True) elif "tmdb_movie_details" in self.backgrounds: set_image("tmdb_movie", self.backgrounds, is_background=True) elif "tmdb_show_details" in self.backgrounds: set_image("tmdb_show", self.backgrounds, is_background=True) - else: logger.info("No background to update") \ No newline at end of file + else: logger.info("No background to update") + + def run_collections_again(self, library, collection_obj, movie_map, show_map): + collection_items = collection_obj.items() if isinstance(collection_obj, Collections) else [] + name = collection_obj.title if isinstance(collection_obj, Collections) else collection_obj + rating_keys = [movie_map[mm] for mm in self.missing_movies if mm in movie_map] + if library.is_show: + rating_keys.extend([show_map[sm] for sm in self.missing_shows if sm in show_map]) + + if len(rating_keys) > 0: + for rating_key in rating_keys: + try: + current = library.fetchItem(int(rating_key)) + except (BadRequest, NotFound): + logger.error(f"Plex Error: Item {rating_key} not found") + continue + if current in collection_items: + logger.info(f"{name} Collection | = | {current.title}") + else: + current.addCollection(name) + logger.info(f"{name} Collection | + | {current.title}") + logger.info(f"{len(rating_keys)} {'Movie' if library.is_movie else 'Show'}{'s' if len(rating_keys) > 1 else ''} Processed") + + if len(self.missing_movies) > 0: + logger.info("") + for missing_id in self.missing_movies: + if missing_id not in movie_map: + try: + movie = self.config.TMDb.get_movie(missing_id) + except Failed as e: + logger.error(e) + continue + if self.details["show_missing"] is True: + logger.info(f"{name} Collection | ? | {movie.title} (TMDb: {missing_id})") + logger.info("") + logger.info(f"{len(self.missing_movies)} Movie{'s' if len(self.missing_movies) > 1 else ''} Missing") + + if len(self.missing_shows) > 0 and library.is_show: + logger.info("") + for missing_id in self.missing_shows: + if missing_id not in show_map: + try: + title = str(self.config.TVDb.get_series(self.library.Plex.language, tvdb_id=missing_id).title.encode("ascii", "replace").decode()) + except Failed as e: + logger.error(e) + continue + if self.details["show_missing"] is True: + logger.info(f"{name} Collection | ? | {title} (TVDb: {missing_id})") + logger.info(f"{len(self.missing_shows)} Show{'s' if len(self.missing_shows) > 1 else ''} Missing") diff --git a/modules/config.py b/modules/config.py index a94f37b8..945e8a73 100644 --- a/modules/config.py +++ b/modules/config.py @@ -1,4 +1,4 @@ -import logging, os, re, requests +import logging, os, re, requests, time from modules import util from modules.anidb import AniDBAPI from modules.builder import CollectionBuilder @@ -152,6 +152,7 @@ class Config: self.general["show_filtered"] = check_for_attribute(self.data, "show_filtered", parent="settings", var_type="bool", default=False) self.general["show_missing"] = check_for_attribute(self.data, "show_missing", parent="settings", var_type="bool", default=True) self.general["save_missing"] = check_for_attribute(self.data, "save_missing", parent="settings", var_type="bool", default=True) + self.general["run_again_delay"] = check_for_attribute(self.data, "run_again_delay", parent="settings", var_type="int", default=0) util.separator() @@ -414,9 +415,13 @@ class Config: builder.update_details(plex_collection) + if builder.run_again and (len(builder.missing_movies) > 0 or len(builder.missing_shows) > 0): + library.run_again.append(builder) + except Exception as e: util.print_stacktrace() logger.error(f"Unknown Error: {e}") + if library.show_unmanaged is True and not test and not requested_collections: logger.info("") util.separator(f"Unmanaged Collections in {library.name} Library") @@ -432,6 +437,44 @@ class Config: logger.info("") logger.error("No collection to update") + has_run_again = False + for library in self.libraries: + if library.run_again: + has_run_again = True + break + + if has_run_again: + logger.info("") + util.separator("Run Again") + logger.info("") + length = 0 + for x in range(1, self.general["run_again_delay"] + 1): + length = util.print_return(length, f"Waiting to run again in {self.general['run_again_delay'] - x + 1} minutes") + for y in range(60): + time.sleep(1) + util.print_end(length) + for library in self.libraries: + if library.run_again: + os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(library.timeout) + logger.info("") + util.separator(f"{library.name} Library Run Again") + logger.info("") + collections = {c: library.collections[c] for c in util.get_list(requested_collections) if c in library.collections} if requested_collections else library.collections + if collections: + util.separator(f"Mapping {library.name} Library") + logger.info("") + movie_map, show_map = self.map_guids(library) + for builder in library.run_again: + logger.info("") + util.separator(f"{builder.name} Collection") + logger.info("") + try: + collection_obj = library.get_collection(builder.name) + except Failed as e: + util.print_multiline(e, error=True) + continue + builder.run_collections_again(library, collection_obj, movie_map, show_map) + def map_guids(self, library): movie_map = {} show_map = {} diff --git a/modules/plex.py b/modules/plex.py index 8993f32e..88c10b89 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -63,6 +63,7 @@ class PlexAPI: self.plex = params["plex"] self.timeout = params["plex"]["timeout"] self.missing = {} + self.run_again = [] def add_Radarr(self, Radarr): self.Radarr = Radarr diff --git a/modules/util.py b/modules/util.py index f7b76581..25a97f30 100644 --- a/modules/util.py +++ b/modules/util.py @@ -264,6 +264,7 @@ collectionless_lists = [ "name_mapping", "label", "label_sync_mode" ] other_attributes = [ + "run_again", "schedule", "sync_mode", "template",