reorganize methods

This commit is contained in:
meisnate12 2021-05-12 10:25:48 -04:00
parent f8e10315d5
commit 6cf158fc10
5 changed files with 393 additions and 402 deletions

View file

@ -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
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:
movie = self.config.TMDb.get_movie(key)
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:
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
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")
logger.info("")
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")
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)
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 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:

View file

@ -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")

View file

@ -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:

View file

@ -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

View file

@ -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: