From 6cf158fc102ade2012d2f4d7e6066cc4dd4271b6 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Wed, 12 May 2021 10:25:48 -0400 Subject: [PATCH] reorganize methods --- modules/builder.py | 425 +++++++++++++++++++++++++++++-------------- modules/cache.py | 1 - modules/plex.py | 142 +-------------- modules/tautulli.py | 19 +- plex_meta_manager.py | 206 +++++++++------------ 5 files changed, 392 insertions(+), 401 deletions(-) diff --git a/modules/builder.py b/modules/builder.py index 3b908964..79ed2570 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -3,6 +3,7 @@ from datetime import datetime, timedelta from modules import anidb, anilist, imdb, letterboxd, mal, plex, radarr, sonarr, tautulli, tmdb, trakttv, tvdb, util from modules.util import Failed from plexapi.exceptions import BadRequest, NotFound +from plexapi.video import Movie, Show from urllib.parse import quote logger = logging.getLogger("Plex Meta Manager") @@ -26,6 +27,20 @@ method_alias = { "writers": "writer", "years": "year" } +filter_alias = { + "actor": "actors", + "audience_rating": "audienceRating", + "collection": "collections", + "content_rating": "contentRating", + "country": "countries", + "critic_rating": "rating", + "director": "directors", + "genre": "genres", + "originally_available": "originallyAvailableAt", + "tmdb_vote_count": "vote_count", + "user_rating": "userRating", + "writer": "writers" +} modifier_alias = {".greater": ".gt", ".less": ".lt"} all_builders = anidb.builders + anilist.builders + imdb.builders + letterboxd.builders + mal.builders + plex.builders + tautulli.builders + tmdb.builders + trakttv.builders + tvdb.builders dictionary_builders = [ @@ -191,6 +206,9 @@ class CollectionBuilder: self.missing_shows = [] self.methods = [] self.filters = [] + self.rating_keys = [] + self.missing_movies = [] + self.missing_shows = [] self.posters = {} self.backgrounds = {} self.summaries = {} @@ -1207,16 +1225,32 @@ class CollectionBuilder: self.details["collection_mode"] = "hide" self.sync = True - def run_methods(self, collection_obj, collection_name, rating_key_map, movie_map, show_map): - items_found = 0 + try: + self.obj = library.get_collection(self.name) + collection_smart = library.smart(self.obj) + if (self.smart and not collection_smart) or (not self.smart and collection_smart): + logger.info("") + logger.error(f"Collection Error: Converting {self.obj.title} to a {'smart' if self.smart else 'normal'} collection") + library.query(self.obj.delete) + self.obj = None + except Failed: + self.obj = None + + self.plex_map = {} + if self.sync and self.obj: + for item in library.get_collection_items(self.obj, self.smart_label_collection): + self.plex_map[item.ratingKey] = item + + def collect_rating_keys(self, movie_map, show_map): + def add_rating_keys(keys): + if not isinstance(keys, list): + keys = [keys] + self.rating_keys.extend([key for key in keys if key not in self.rating_keys]) for method, values in self.methods: logger.debug("") logger.debug(f"Method: {method}") logger.debug(f"Values: {values}") for value in values: - items = [] - missing_movies = [] - missing_shows = [] def check_map(input_ids): movie_ids, show_ids = input_ids items_found_inside = 0 @@ -1224,134 +1258,252 @@ class CollectionBuilder: items_found_inside += len(movie_ids) for movie_id in movie_ids: if movie_id in movie_map: - items.extend(movie_map[movie_id]) - else: - missing_movies.append(movie_id) + add_rating_keys(movie_map[movie_id]) + elif movie_id not in self.missing_movies: + self.missing_movies.append(movie_id) if len(show_ids) > 0: items_found_inside += len(show_ids) for show_id in show_ids: if show_id in show_map: - items.extend(show_map[show_id]) - else: - missing_shows.append(show_id) + add_rating_keys(show_map[show_id]) + elif show_id not in self.missing_shows: + self.missing_shows.append(show_id) return items_found_inside logger.debug("") logger.debug(f"Value: {value}") logger.info("") - if "plex" in method: - items = self.library.get_items(method, value) - items_found += len(items) - elif "tautulli" in method: - items = self.library.Tautulli.get_items(self.library, time_range=value["list_days"], stats_count=value["list_size"], list_type=value["list_type"], stats_count_buffer=value["list_buffer"]) - items_found += len(items) - elif "anidb" in method: items_found += check_map(self.config.AniDB.get_items(method, value, self.library.Plex.language)) - elif "anilist" in method: items_found += check_map(self.config.AniList.get_items(method, value)) - elif "mal" in method: items_found += check_map(self.config.MyAnimeList.get_items(method, value)) - elif "tvdb" in method: items_found += check_map(self.config.TVDb.get_items(method, value, self.library.Plex.language)) - elif "imdb" in method: items_found += check_map(self.config.IMDb.get_items(method, value, self.library.Plex.language)) - elif "letterboxd" in method: items_found += check_map(self.config.Letterboxd.get_items(method, value, self.library.Plex.language)) - elif "tmdb" in method: items_found += check_map(self.config.TMDb.get_items(method, value, self.library.is_movie)) - elif "trakt" in method: items_found += check_map(self.config.Trakt.get_items(method, value, self.library.is_movie)) + if "plex" in method: add_rating_keys(self.library.get_items(method, value)) + elif "tautulli" in method: add_rating_keys(self.library.Tautulli.get_items(self.library, value)) + elif "anidb" in method: check_map(self.config.AniDB.get_items(method, value, self.library.Plex.language)) + elif "anilist" in method: check_map(self.config.AniList.get_items(method, value)) + elif "mal" in method: check_map(self.config.MyAnimeList.get_items(method, value)) + elif "tvdb" in method: check_map(self.config.TVDb.get_items(method, value, self.library.Plex.language)) + elif "imdb" in method: check_map(self.config.IMDb.get_items(method, value, self.library.Plex.language)) + elif "letterboxd" in method: check_map(self.config.Letterboxd.get_items(method, value, self.library.Plex.language)) + elif "tmdb" in method: check_map(self.config.TMDb.get_items(method, value, self.library.is_movie)) + elif "trakt" in method: check_map(self.config.Trakt.get_items(method, value, self.library.is_movie)) else: logger.error(f"Collection Error: {method} method not supported") - logger.info("") - if len(items) > 0: - rating_key_map = self.library.add_to_collection(collection_obj if collection_obj else collection_name, items, self.filters, self.details["show_filtered"], self.smart_label_collection, rating_key_map, movie_map, show_map) - else: - logger.error("No items found to add to this collection ") - - if len(missing_movies) > 0 or len(missing_shows) > 0: - logger.info("") - arr_filters = [] - for filter_method, filter_data in self.filters: - if (filter_method.startswith("original_language") and self.library.is_movie) or filter_method.startswith("tmdb_vote_count"): - arr_filters.append((filter_method, filter_data)) - if len(missing_movies) > 0: - missing_movies_with_names = [] - for missing_id in missing_movies: - try: - movie = self.config.TMDb.get_movie(missing_id) - except Failed as e: - logger.error(e) - continue - match = True - for filter_method, filter_data in arr_filters: - if (filter_method == "original_language" and movie.original_language not in filter_data) \ - or (filter_method == "original_language.not" and movie.original_language in filter_data) \ - or (filter_method == "tmdb_vote_count.gte" and movie.vote_count < filter_data) \ - or (filter_method == "tmdb_vote_count.lte" and movie.vote_count > filter_data): - match = False - break - if match: - missing_movies_with_names.append((movie.title, missing_id)) - if self.details["show_missing"] is True: - logger.info(f"{collection_name} Collection | ? | {movie.title} (TMDb: {missing_id})") - elif self.details["show_filtered"] is True: - logger.info(f"{collection_name} Collection | X | {movie.title} (TMDb: {missing_id})") - logger.info(f"{len(missing_movies_with_names)} Movie{'s' if len(missing_movies_with_names) > 1 else ''} Missing") - if self.details["save_missing"] is True: - self.library.add_missing(collection_name, missing_movies_with_names, True) - if (self.add_to_radarr and self.library.Radarr) or self.run_again: - missing_tmdb_ids = [missing_id for title, missing_id in missing_movies_with_names] - if self.add_to_radarr and self.library.Radarr: + def add_to_collection(self, movie_map, show_map): + name, collection_items = self.library.get_collection_name_and_items(self.obj if self.obj else self.name, self.smart_label_collection) + total = len(self.rating_keys) + max_length = len(str(total)) + length = 0 + for i, item in enumerate(self.rating_keys, 1): + try: + current = self.library.fetchItem(item.ratingKey if isinstance(item, (Movie, Show)) else int(item)) + if not isinstance(current, (Movie, Show)): + raise NotFound + except (BadRequest, NotFound): + logger.error(f"Plex Error: Item {item} not found") + continue + match = True + if self.filters: + length = util.print_return(length, f"Filtering {(' ' * (max_length - len(str(i)))) + str(i)}/{total} {current.title}") + for filter_method, filter_data in self.filters: + modifier = filter_method[-4:] + method = filter_method[:-4] if modifier in [".not", ".lte", ".gte"] else filter_method + method_name = filter_alias[method] if method in filter_alias else method + if method_name == "max_age": + threshold_date = datetime.now() - timedelta(days=filter_data) + if current.originallyAvailableAt is None or current.originallyAvailableAt < threshold_date: + match = False + break + elif method_name == "original_language": + movie = None + for key, value in movie_map.items(): + if current.ratingKey in value: try: - self.library.Radarr.add_tmdb(missing_tmdb_ids, **self.radarr_options) - except Failed as e: - logger.error(e) - if self.run_again: - self.missing_movies.extend(missing_tmdb_ids) - if len(missing_shows) > 0 and self.library.is_show: - missing_shows_with_names = [] - for missing_id in missing_shows: - try: - title = str(self.config.TVDb.get_series(self.library.Plex.language, missing_id).title.encode("ascii", "replace").decode()) - except Failed as e: - logger.error(e) - continue - match = True - if arr_filters: - show = self.config.TMDb.get_show(self.config.Convert.tvdb_to_tmdb(missing_id)) - for filter_method, filter_data in arr_filters: - if (filter_method == "tmdb_vote_count.gte" and show.vote_count < filter_data) \ - or (filter_method == "tmdb_vote_count.lte" and show.vote_count > filter_data): - match = False + movie = self.config.TMDb.get_movie(key) + break + except Failed: + pass + if movie is None: + logger.warning(f"Filter Error: No TMDb ID found for {current.title}") + continue + if (modifier == ".not" and movie.original_language in filter_data) or ( + modifier != ".not" and movie.original_language not in filter_data): + match = False + break + elif method_name == "audio_track_title": + jailbreak = False + for media in current.media: + for part in media.parts: + for audio in part.audioStreams(): + for check_title in filter_data: + title = audio.title if audio.title else "" + if check_title.lower() in title.lower(): + jailbreak = True + break + if jailbreak: break + if jailbreak: break + if jailbreak: break + if (jailbreak and modifier == ".not") or (not jailbreak and modifier != ".not"): + match = False + break + elif method_name == "filepath": + jailbreak = False + for location in current.locations: + for check_text in filter_data: + if check_text.lower() in location.lower(): + jailbreak = True + break + if jailbreak: break + if (jailbreak and modifier == ".not") or (not jailbreak and modifier != ".not"): + match = False + break + elif modifier in [".gte", ".lte"]: + if method_name == "vote_count": + tmdb_item = None + for key, value in movie_map.items(): + if current.ratingKey in value: + try: + tmdb_item = self.config.TMDb.get_movie(key) if self.library.is_movie else self.config.TMDb.get_show(key) break - if match: - missing_shows_with_names.append((title, missing_id)) - 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} (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.add_to_sonarr and self.library.Sonarr) or self.run_again: - missing_tvdb_ids = [missing_id for title, missing_id in missing_shows_with_names] - if self.add_to_sonarr and self.library.Sonarr: - try: - self.library.Sonarr.add_tvdb(missing_tvdb_ids, **self.sonarr_options) - except Failed as e: - logger.error(e) - if self.run_again: - self.missing_shows.extend(missing_tvdb_ids) - - if self.sync and items_found > 0: - logger.info("") - count_removed = 0 - for ratingKey, item in rating_key_map.items(): - if item is not None: - logger.info(f"{collection_name} Collection | - | {item.title}") - if self.smart_label_collection: - self.library.query_data(item.removeLabel, collection_name) + except Failed: + pass + if tmdb_item is None: + logger.warning(f"Filter Error: No TMDb ID found for {current.title}") + continue + attr = tmdb_item.vote_count + else: + attr = getattr(current, method_name) / 60000 if method_name == "duration" else getattr(current, method_name) + if attr is None or (modifier == ".lte" and attr > filter_data) or (modifier == ".gte" and attr < filter_data): + match = False + break else: - self.library.query_data(item.removeCollection, collection_name) - count_removed += 1 - logger.info(f"{count_removed} {'Movie' if self.library.is_movie else 'Show'}{'s' if count_removed == 1 else ''} Removed") + attrs = [] + if method_name in ["video_resolution", "audio_language", "subtitle_language"]: + for media in current.media: + if method_name == "video_resolution": + attrs.extend([media.videoResolution]) + for part in media.parts: + if method_name == "audio_language": + attrs.extend([a.language for a in part.audioStreams()]) + if method_name == "subtitle_language": + attrs.extend([s.language for s in part.subtitleStreams()]) + elif method_name in ["contentRating", "studio", "year", "rating", "originallyAvailableAt"]: + attrs = [str(getattr(current, method_name))] + elif method_name in ["actors", "countries", "directors", "genres", "writers", "collections"]: + attrs = [getattr(x, "tag") for x in getattr(current, method_name)] + else: + raise Failed(f"Filter Error: filter: {method_name} not supported") + + if (not list(set(filter_data) & set(attrs)) and modifier != ".not")\ + or (list(set(filter_data) & set(attrs)) and modifier == ".not"): + match = False + break + length = util.print_return(length, f"Filtering {(' ' * (max_length - len(str(i)))) + str(i)}/{total} {current.title}") + if match: + util.print_end(length, f"{name} Collection | {'=' if current in collection_items else '+'} | {current.title}") + if current in collection_items: + self.plex_map[current.ratingKey] = None + elif self.smart_label_collection: + self.library.query_data(current.addLabel, name) + else: + self.library.query_data(current.addCollection, name) + elif self.details["show_filtered"] is True: + logger.info(f"{name} Collection | X | {current.title}") + media_type = f"{'Movie' if self.library.is_movie else 'Show'}{'s' if total > 1 else ''}" + util.print_end(length, f"{total} {media_type} Processed") + + def run_missing(self, missing_movies, missing_shows): logger.info("") + arr_filters = [] + for filter_method, filter_data in self.filters: + if (filter_method.startswith("original_language") and self.library.is_movie) or filter_method.startswith("tmdb_vote_count"): + arr_filters.append((filter_method, filter_data)) + if len(missing_movies) > 0: + missing_movies_with_names = [] + for missing_id in missing_movies: + try: + movie = self.config.TMDb.get_movie(missing_id) + except Failed as e: + logger.error(e) + continue + match = True + for filter_method, filter_data in arr_filters: + if (filter_method == "original_language" and movie.original_language not in filter_data) \ + or (filter_method == "original_language.not" and movie.original_language in filter_data) \ + or (filter_method == "tmdb_vote_count.gte" and movie.vote_count < filter_data) \ + or (filter_method == "tmdb_vote_count.lte" and movie.vote_count > filter_data): + match = False + break + if match: + missing_movies_with_names.append((movie.title, missing_id)) + if self.details["show_missing"] is True: + logger.info(f"{self.name} Collection | ? | {movie.title} (TMDb: {missing_id})") + elif self.details["show_filtered"] is True: + logger.info(f"{self.name} Collection | X | {movie.title} (TMDb: {missing_id})") + logger.info(f"{len(missing_movies_with_names)} Movie{'s' if len(missing_movies_with_names) > 1 else ''} Missing") + if self.details["save_missing"] is True: + self.library.add_missing(self.name, missing_movies_with_names, True) + if (self.add_to_radarr and self.library.Radarr) or self.run_again: + missing_tmdb_ids = [missing_id for title, missing_id in missing_movies_with_names] + if self.add_to_radarr and self.library.Radarr: + try: + self.library.Radarr.add_tmdb(missing_tmdb_ids, **self.radarr_options) + except Failed as e: + logger.error(e) + if self.run_again: + self.missing_movies.extend(missing_tmdb_ids) + if len(missing_shows) > 0 and self.library.is_show: + missing_shows_with_names = [] + for missing_id in missing_shows: + try: + title = str(self.config.TVDb.get_series(self.library.Plex.language, missing_id).title.encode("ascii", "replace").decode()) + except Failed as e: + logger.error(e) + continue + match = True + if arr_filters: + show = self.config.TMDb.get_show(self.config.Convert.tvdb_to_tmdb(missing_id)) + for filter_method, filter_data in arr_filters: + if (filter_method == "tmdb_vote_count.gte" and show.vote_count < filter_data) \ + or (filter_method == "tmdb_vote_count.lte" and show.vote_count > filter_data): + match = False + break + if match: + missing_shows_with_names.append((title, missing_id)) + if self.details["show_missing"] is True: + logger.info(f"{self.name} Collection | ? | {title} (TVDB: {missing_id})") + elif self.details["show_filtered"] is True: + logger.info(f"{self.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(self.name, missing_shows_with_names, False) + if (self.add_to_sonarr and self.library.Sonarr) or self.run_again: + missing_tvdb_ids = [missing_id for title, missing_id in missing_shows_with_names] + if self.add_to_sonarr and self.library.Sonarr: + try: + self.library.Sonarr.add_tvdb(missing_tvdb_ids, **self.sonarr_options) + except Failed as e: + logger.error(e) + if self.run_again: + self.missing_shows.extend(missing_tvdb_ids) - def update_details(self, collection): - if self.smart_url and self.smart_url != self.library.smart_filter(collection): - self.library.update_smart_collection(collection, self.smart_url) + def sync_collection(self): + logger.info("") + count_removed = 0 + for ratingKey, item in self.rating_key_map.items(): + if item is not None: + logger.info(f"{self.name} Collection | - | {item.title}") + if self.smart_label_collection: + self.library.query_data(item.removeLabel, self.name) + else: + self.library.query_data(item.removeCollection, self.name) + count_removed += 1 + logger.info(f"{count_removed} {'Movie' if self.library.is_movie else 'Show'}{'s' if count_removed == 1 else ''} Removed") + + def update_details(self): + if not self.obj and self.smart_url: + self.library.create_smart_collection(self.name, self.smart_type_key, self.smart_url) + elif not self.obj and self.smart_label_collection: + self.library.create_smart_labels(self.name, sort=self.smart_sort) + self.obj = self.library.get_collection(self.name) + + if self.smart_url and self.smart_url != self.library.smart_filter(self.obj): + self.library.update_smart_collection(self.obj, self.smart_url) logger.info(f"Detail: Smart Filter updated to {self.smart_url}") edits = {} @@ -1379,50 +1531,50 @@ class CollectionBuilder: elif "tmdb_show_details" in self.summaries: summary = get_summary("tmdb_show_details", self.summaries) else: summary = None if summary: - if str(summary) != str(collection.summary): + if str(summary) != str(self.obj.summary): edits["summary.value"] = summary edits["summary.locked"] = 1 if "sort_title" in self.details: - if str(self.details["sort_title"]) != str(collection.titleSort): + if str(self.details["sort_title"]) != str(self.obj.titleSort): edits["titleSort.value"] = self.details["sort_title"] edits["titleSort.locked"] = 1 logger.info(f"Detail: sort_title updated Collection Sort Title to {self.details['sort_title']}") if "content_rating" in self.details: - if str(self.details["content_rating"]) != str(collection.contentRating): + if str(self.details["content_rating"]) != str(self.obj.contentRating): edits["contentRating.value"] = self.details["content_rating"] edits["contentRating.locked"] = 1 logger.info(f"Detail: content_rating updated Collection Content Rating to {self.details['content_rating']}") if "collection_mode" in self.details: - if int(collection.collectionMode) not in plex.collection_mode_keys\ - or plex.collection_mode_keys[int(collection.collectionMode)] != self.details["collection_mode"]: - self.library.collection_mode_query(collection, self.details["collection_mode"]) + if int(self.obj.collectionMode) not in plex.collection_mode_keys\ + or plex.collection_mode_keys[int(self.obj.collectionMode)] != self.details["collection_mode"]: + self.library.collection_mode_query(self.obj, self.details["collection_mode"]) logger.info(f"Detail: collection_mode updated Collection Mode to {self.details['collection_mode']}") if "collection_order" in self.details: - if int(collection.collectionSort) not in plex.collection_order_keys\ - or plex.collection_order_keys[int(collection.collectionSort)] != self.details["collection_order"]: - self.library.collection_order_query(collection, self.details["collection_order"]) + if int(self.obj.collectionSort) not in plex.collection_order_keys\ + or plex.collection_order_keys[int(self.obj.collectionSort)] != self.details["collection_order"]: + self.library.collection_order_query(self.obj, self.details["collection_order"]) logger.info(f"Detail: collection_order updated Collection Order to {self.details['collection_order']}") if "label" in self.details or "label.sync" in self.details: - item_labels = [label.tag for label in collection.labels] + item_labels = [label.tag for label in self.obj.labels] labels = util.get_list(self.details["label" if "label" in self.details else "label.sync"]) if "label.sync" in self.details: for label in (la for la in item_labels if la not in labels): - self.library.query_data(collection.removeLabel, label) + self.library.query_data(self.obj.removeLabel, label) logger.info(f"Detail: Label {label} removed") for label in (la for la in labels if la not in item_labels): - self.library.query_data(collection.addLabel, label) + self.library.query_data(self.obj.addLabel, label) logger.info(f"Detail: Label {label} added") if len(self.item_details) > 0: labels = None if "item_label" in self.item_details or "item_label.sync" in self.item_details: labels = util.get_list(self.item_details["item_label" if "item_label" in self.item_details else "item_label.sync"]) - for item in self.library.get_collection_items(collection, self.smart_label_collection): + for item in self.library.get_collection_items(self.obj, self.smart_label_collection): if labels is not None: item_labels = [label.tag for label in item.labels] if "item_label.sync" in self.item_details: @@ -1442,7 +1594,7 @@ class CollectionBuilder: if len(edits) > 0: logger.debug(edits) - self.library.edit_query(collection, edits) + self.library.edit_query(self.obj, edits) logger.info("Details: have been updated") if self.library.asset_directory: @@ -1466,13 +1618,13 @@ class CollectionBuilder: matches = glob.glob(background_filter) if len(matches) > 0: self.backgrounds["asset_directory"] = os.path.abspath(matches[0]) - for item in self.library.query(collection.items): + for item in self.library.query(self.obj.items): self.library.update_item_from_assets(item, dirs=[path]) def set_image(image_method, images, is_background=False): message = f"{'background' if is_background else 'poster'} to [{'File' if image_method in image_file_details else 'URL'}] {images[image_method]}" try: - self.library.upload_image(collection, images[image_method], poster=not is_background, url=image_method not in image_file_details) + self.library.upload_image(self.obj, images[image_method], poster=not is_background, url=image_method not in image_file_details) logger.info(f"Detail: {image_method} updated collection {message}") except BadRequest: logger.error(f"Detail: {image_method} failed to update {message}") @@ -1524,8 +1676,9 @@ class CollectionBuilder: elif "tmdb_show_details" in self.backgrounds: set_image("tmdb_show_details", self.backgrounds, is_background=True) else: logger.info("No background to update") - def run_collections_again(self, collection_obj, movie_map, show_map): - name, collection_items = self.library.get_collection_name_and_items(collection_obj, self.smart_label_collection) + def run_collections_again(self, movie_map, show_map): + self.obj = self.library.get_collection(self.name) + name, collection_items = self.library.get_collection_name_and_items(self.obj, self.smart_label_collection) rating_keys = [] for mm in self.missing_movies: if mm in movie_map: diff --git a/modules/cache.py b/modules/cache.py index 9bdf82a6..df3b5b92 100644 --- a/modules/cache.py +++ b/modules/cache.py @@ -1,7 +1,6 @@ import logging, os, random, sqlite3 from contextlib import closing from datetime import datetime, timedelta -from modules.util import Failed logger = logging.getLogger("Plex Meta Manager") diff --git a/modules/plex.py b/modules/plex.py index 00afd21c..cca7d77c 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -7,7 +7,6 @@ from plexapi import utils from plexapi.exceptions import BadRequest, NotFound, Unauthorized from plexapi.collection import Collections from plexapi.server import PlexServer -from plexapi.video import Movie, Show from retrying import retry from ruamel import yaml from urllib import parse @@ -77,20 +76,6 @@ item_advance_keys = { "item_use_original_title": ("useOriginalTitle", use_original_title_options) } new_plex_agents = ["tv.plex.agents.movie", "tv.plex.agents.series"] -filter_alias = { - "actor": "actors", - "audience_rating": "audienceRating", - "collection": "collections", - "content_rating": "contentRating", - "country": "countries", - "critic_rating": "rating", - "director": "directors", - "genre": "genres", - "originally_available": "originallyAvailableAt", - "tmdb_vote_count": "vote_count", - "user_rating": "userRating", - "writer": "writers" -} searches = [ "title", "title.and", "title.not", "title.begins", "title.ends", "studio", "studio.and", "studio.not", "studio.begins", "studio.ends", @@ -367,8 +352,12 @@ class PlexAPI: return self.Plex.search(title=title, sort=sort, maxresults=maxresults, libtype=libtype, **kwargs) @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex) - def exact_search(self, title, libtype=None): - return self.Plex.search(libtype=libtype, **{"title=": title}) + def exact_search(self, title, libtype=None, year=None): + if year: + terms = {"title=": title, "year": year} + else: + terms = {"title=": title} + return self.Plex.search(libtype=libtype, **terms) @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex) def get_labeled_items(self, label): @@ -652,125 +641,6 @@ class PlexAPI: name = collection.title if isinstance(collection, Collections) else str(collection) return name, self.get_collection_items(collection, smart_label_collection) - def add_to_collection(self, collection, items, filters, show_filtered, smart, rating_key_map, movie_map, show_map): - name, collection_items = self.get_collection_name_and_items(collection, smart) - total = len(items) - max_length = len(str(total)) - length = 0 - for i, item in enumerate(items, 1): - try: - current = self.fetchItem(item.ratingKey if isinstance(item, (Movie, Show)) else int(item)) - if not isinstance(current, (Movie, Show)): - raise NotFound - except (BadRequest, NotFound): - logger.error(f"Plex Error: Item {item} not found") - continue - match = True - if filters: - length = util.print_return(length, f"Filtering {(' ' * (max_length - len(str(i)))) + str(i)}/{total} {current.title}") - for filter_method, filter_data in filters: - modifier = filter_method[-4:] - method = filter_method[:-4] if modifier in [".not", ".lte", ".gte"] else filter_method - method_name = filter_alias[method] if method in filter_alias else method - if method_name == "max_age": - threshold_date = datetime.now() - timedelta(days=filter_data) - if current.originallyAvailableAt is None or current.originallyAvailableAt < threshold_date: - match = False - break - elif method_name == "original_language": - movie = None - for key, value in movie_map.items(): - if current.ratingKey in value: - try: - movie = self.TMDb.get_movie(key) - break - except Failed: - pass - if movie is None: - logger.warning(f"Filter Error: No TMDb ID found for {current.title}") - continue - if (modifier == ".not" and movie.original_language in filter_data) or (modifier != ".not" and movie.original_language not in filter_data): - match = False - break - elif method_name == "audio_track_title": - jailbreak = False - for media in current.media: - for part in media.parts: - for audio in part.audioStreams(): - for check_title in filter_data: - title = audio.title if audio.title else "" - if check_title.lower() in title.lower(): - jailbreak = True - break - if jailbreak: break - if jailbreak: break - if jailbreak: break - if (jailbreak and modifier == ".not") or (not jailbreak and modifier != ".not"): - match = False - break - elif method_name == "filepath": - jailbreak = False - for location in current.locations: - for check_text in filter_data: - if check_text.lower() in location.lower(): - jailbreak = True - break - if jailbreak: break - if (jailbreak and modifier == ".not") or (not jailbreak and modifier != ".not"): - match = False - break - elif modifier in [".gte", ".lte"]: - if method_name == "vote_count": - tmdb_item = None - for key, value in movie_map.items(): - if current.ratingKey in value: - try: - tmdb_item = self.TMDb.get_movie(key) if self.is_movie else self.TMDb.get_show(key) - break - except Failed: - pass - if tmdb_item is None: - logger.warning(f"Filter Error: No TMDb ID found for {current.title}") - continue - attr = tmdb_item.vote_count - else: - attr = getattr(current, method_name) / 60000 if method_name == "duration" else getattr(current, method_name) - if attr is None or (modifier == ".lte" and attr > filter_data) or (modifier == ".gte" and attr < filter_data): - match = False - break - else: - attrs = [] - if method_name in ["video_resolution", "audio_language", "subtitle_language"]: - for media in current.media: - if method_name == "video_resolution": - attrs.extend([media.videoResolution]) - for part in media.parts: - if method_name == "audio_language": - attrs.extend([a.language for a in part.audioStreams()]) - if method_name == "subtitle_language": - attrs.extend([s.language for s in part.subtitleStreams()]) - elif method_name in ["contentRating", "studio", "year", "rating", "originallyAvailableAt"]: - attrs = [str(getattr(current, method_name))] - elif method_name in ["actors", "countries", "directors", "genres", "writers", "collections"]: - attrs = [getattr(x, "tag") for x in getattr(current, method_name)] - else: - raise Failed(f"Filter Error: filter: {method_name} not supported") - - if (not list(set(filter_data) & set(attrs)) and modifier != ".not") or (list(set(filter_data) & set(attrs)) and modifier == ".not"): - match = False - break - length = util.print_return(length, f"Filtering {(' ' * (max_length - len(str(i)))) + str(i)}/{total} {current.title}") - if match: - util.print_end(length, f"{name} Collection | {'=' if current in collection_items else '+'} | {current.title}") - if current in collection_items: rating_key_map[current.ratingKey] = None - elif smart: self.query_data(current.addLabel, name) - else: self.query_data(current.addCollection, name) - elif show_filtered is True: - logger.info(f"{name} Collection | X | {current.title}") - media_type = f"{'Movie' if self.is_movie else 'Show'}{'s' if total > 1 else ''}" - util.print_end(length, f"{total} {media_type} Processed") - return rating_key_map - def search_item(self, data, year=None): kwargs = {} if year is not None: diff --git a/modules/tautulli.py b/modules/tautulli.py index 5e2f2441..cd2e17c5 100644 --- a/modules/tautulli.py +++ b/modules/tautulli.py @@ -21,10 +21,11 @@ class TautulliAPI: self.url = params["url"] self.apikey = params["apikey"] - def get_items(self, library, time_range=30, stats_count=20, list_type="popular", stats_count_buffer=20): - logger.info(f"Processing Tautulli Most {'Popular' if list_type == 'popular' else 'Watched'}: {stats_count} {'Movies' if library.is_movie else 'Shows'}") - response = self._request(f"{self.url}/api/v2?apikey={self.apikey}&cmd=get_home_stats&time_range={time_range}&stats_count={int(stats_count) + int(stats_count_buffer)}") - stat_id = f"{'popular' if list_type == 'popular' else 'top'}_{'movies' if library.is_movie else 'tv'}" + def get_items(self, library, params): + query_size = int(params["list_size"]) + int(params["list_buffer"]) + logger.info(f"Processing Tautulli Most {params['list_type'].capitalize()}: {params['list_size']} {'Movies' if library.is_movie else 'Shows'}") + response = self._request(f"{self.url}/api/v2?apikey={self.apikey}&cmd=get_home_stats&time_range={params['list_days']}&stats_count={query_size}") + stat_id = f"{'popular' if params['list_type'] == 'popular' else 'top'}_{'movies' if library.is_movie else 'tv'}" items = None for entry in response["response"]["data"]: @@ -38,19 +39,17 @@ class TautulliAPI: rating_keys = [] count = 0 for item in items: - if item["section_id"] == section_id and count < int(stats_count): - rk = None + if item["section_id"] == section_id and count < int(params['list_size']): try: library.fetchItem(int(item["rating_key"])) - rk = item["rating_key"] + rating_keys.append(item["rating_key"]) except (BadRequest, NotFound): - new_item = library.exact_search(item["title"]) + new_item = library.exact_search(item["title"], year=item["year"]) if new_item: - rk = new_item[0].ratingKey + rating_keys.append(new_item[0].ratingKey) else: logger.error(f"Plex Error: Item {item} not found") continue - rating_keys.append(rk) count += 1 return rating_keys diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 9047b47b..1bfa5313 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -186,105 +186,37 @@ def update_libraries(config, is_test, requested_collections, resume_from): util.separator(f"{builder.name} Collection") logger.info("") try: - collection_obj = library.get_collection(builder.name) + builder.run_collections_again(movie_map, show_map) except Failed as e: + util.print_stacktrace() util.print_multiline(e, error=True) - continue - builder.run_collections_again(collection_obj, movie_map, show_map) -def run_collection(config, library, metadata, requested_collections, is_test, resume_from, movie_map, show_map): - for mapping_name, collection_attrs in requested_collections.items(): - if is_test and ("test" not in collection_attrs or collection_attrs["test"] is not True): - no_template_test = True - if "template" in collection_attrs and collection_attrs["template"]: - for data_template in util.get_list(collection_attrs["template"], split=False): - if "name" in data_template \ - and data_template["name"] \ - and metadata.templates \ - and data_template["name"] in metadata.templates \ - and metadata.templates[data_template["name"]] \ - and "test" in metadata.templates[data_template["name"]] \ - and metadata.templates[data_template["name"]]["test"] is True: - no_template_test = False - if no_template_test: - continue +def map_guids(config, library): + movie_map = {} + show_map = {} + length = 0 + logger.info(f"Mapping {'Movie' if library.is_movie else 'Show'} Library: {library.name}") + items = library.Plex.all() + for i, item in enumerate(items, 1): + length = util.print_return(length, f"Processing: {i}/{len(items)} {item.title}") try: - if resume_from and resume_from != mapping_name: - continue - elif resume_from == mapping_name: - resume_from = None - logger.info("") - util.separator(f"Resuming Collections") - - logger.info("") - util.separator(f"{mapping_name} Collection") - logger.info("") - - try: - builder = CollectionBuilder(config, library, metadata, mapping_name, collection_attrs) - except Failed as f: - util.print_stacktrace() - util.print_multiline(f, error=True) - continue - except Exception as e: - util.print_stacktrace() - logger.error(e) - continue - - try: - collection_obj = library.get_collection(mapping_name) - collection_name = collection_obj.title - collection_smart = library.smart(collection_obj) - if (builder.smart and not collection_smart) or (not builder.smart and collection_smart): - logger.info("") - logger.error(f"Collection Error: Converting {collection_obj.title} to a {'smart' if builder.smart else 'normal'} collection") - library.query(collection_obj.delete) - collection_obj = None - except Failed: - collection_obj = None - collection_name = mapping_name - - if len(builder.schedule) > 0: - util.print_multiline(builder.schedule, info=True) - - rating_key_map = {} - logger.info("") - if builder.sync: - logger.info("Sync Mode: sync") - if collection_obj: - for item in library.get_collection_items(collection_obj, builder.smart_label_collection): - rating_key_map[item.ratingKey] = item - else: - logger.info("Sync Mode: append") - - for i, f in enumerate(builder.filters): - if i == 0: - logger.info("") - logger.info(f"Collection Filter {f[0]}: {f[1]}") - - if not builder.smart_url: - builder.run_methods(collection_obj, collection_name, rating_key_map, movie_map, show_map) - - try: - if not collection_obj and builder.smart_url: - library.create_smart_collection(collection_name, builder.smart_type_key, builder.smart_url) - elif not collection_obj and builder.smart_label_collection: - library.create_smart_labels(collection_name, sort=builder.smart_sort) - plex_collection = library.get_collection(collection_name) - except Failed as e: - util.print_stacktrace() - logger.error(e) - continue - - 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: + id_type, main_id = config.Convert.get_id(item, library, length) + except BadRequest: util.print_stacktrace() - logger.error(f"Unknown Error: {e}") - return resume_from + util.print_end(length, f"{'Cache | ! |' if config.Cache else 'Mapping Error:'} | {item.guid} for {item.title} not found") + continue + if not isinstance(main_id, list): + main_id = [main_id] + if id_type == "movie": + for m in main_id: + if m in movie_map: movie_map[m].append(item.ratingKey) + else: movie_map[m] = [item.ratingKey] + elif id_type == "show": + for m in main_id: + if m in show_map: show_map[m].append(item.ratingKey) + else: show_map[m] = [item.ratingKey] + util.print_end(length, f"Processed {len(items)} {'Movies' if library.is_movie else 'Shows'}") + return movie_map, show_map def mass_metadata(config, library, movie_map, show_map): length = 0 @@ -380,32 +312,70 @@ def mass_metadata(config, library, movie_map, show_map): except Failed: pass -def map_guids(config, library): - movie_map = {} - show_map = {} - length = 0 - logger.info(f"Mapping {'Movie' if library.is_movie else 'Show'} Library: {library.name}") - items = library.Plex.all() - for i, item in enumerate(items, 1): - length = util.print_return(length, f"Processing: {i}/{len(items)} {item.title}") +def run_collection(config, library, metadata, requested_collections, is_test, resume_from, movie_map, show_map): + for mapping_name, collection_attrs in requested_collections.items(): + if is_test and ("test" not in collection_attrs or collection_attrs["test"] is not True): + no_template_test = True + if "template" in collection_attrs and collection_attrs["template"]: + for data_template in util.get_list(collection_attrs["template"], split=False): + if "name" in data_template \ + and data_template["name"] \ + and metadata.templates \ + and data_template["name"] in metadata.templates \ + and metadata.templates[data_template["name"]] \ + and "test" in metadata.templates[data_template["name"]] \ + and metadata.templates[data_template["name"]]["test"] is True: + no_template_test = False + if no_template_test: + continue try: - id_type, main_id = config.Convert.get_id(item, library, length) - except BadRequest: + if resume_from and resume_from != mapping_name: + continue + elif resume_from == mapping_name: + resume_from = None + logger.info("") + util.separator(f"Resuming Collections") + + logger.info("") + util.separator(f"{mapping_name} Collection") + logger.info("") + + builder = CollectionBuilder(config, library, metadata, mapping_name, collection_attrs) + + if len(builder.schedule) > 0: + util.print_multiline(builder.schedule, info=True) + + logger.info("") + logger.info(f"Sync Mode: {'sync' if builder.sync else 'append'}") + + if len(builder.filters) > 0: + logger.info("") + for filter_key, filter_value in builder.filters: + logger.info(f"Collection Filter {filter_key}: {filter_value}") + + if not builder.smart_url: + builder.collect_rating_keys(movie_map, show_map) + logger.info("") + if len(builder.rating_keys) > 0: + builder.add_to_collection(movie_map, show_map) + if len(builder.missing_movies) > 0 or len(builder.missing_shows) > 0: + builder.run_missing(movie_map, show_map) + if builder.sync and len(builder.rating_keys) > 0: + builder.sync_collection() + logger.info("") + + builder.update_details() + + if builder.run_again and (len(builder.missing_movies) > 0 or len(builder.missing_shows) > 0): + library.run_again.append(builder) + + except Failed as e: util.print_stacktrace() - util.print_end(length, f"{'Cache | ! |' if config.Cache else 'Mapping Error:'} | {item.guid} for {item.title} not found") - continue - if not isinstance(main_id, list): - main_id = [main_id] - if id_type == "movie": - for m in main_id: - if m in movie_map: movie_map[m].append(item.ratingKey) - else: movie_map[m] = [item.ratingKey] - elif id_type == "show": - for m in main_id: - if m in show_map: show_map[m].append(item.ratingKey) - else: show_map[m] = [item.ratingKey] - util.print_end(length, f"Processed {len(items)} {'Movies' if library.is_movie else 'Shows'}") - return movie_map, show_map + util.print_multiline(e, error=True) + except Exception as e: + util.print_stacktrace() + logger.error(f"Unknown Error: {e}") + return resume_from try: if run or test or collections or libraries or resume: