reorganize methods

pull/240/head
meisnate12 4 years ago
parent f8e10315d5
commit 6cf158fc10

@ -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 import anidb, anilist, imdb, letterboxd, mal, plex, radarr, sonarr, tautulli, tmdb, trakttv, tvdb, util
from modules.util import Failed from modules.util import Failed
from plexapi.exceptions import BadRequest, NotFound from plexapi.exceptions import BadRequest, NotFound
from plexapi.video import Movie, Show
from urllib.parse import quote from urllib.parse import quote
logger = logging.getLogger("Plex Meta Manager") logger = logging.getLogger("Plex Meta Manager")
@ -26,6 +27,20 @@ method_alias = {
"writers": "writer", "writers": "writer",
"years": "year" "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"} 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 all_builders = anidb.builders + anilist.builders + imdb.builders + letterboxd.builders + mal.builders + plex.builders + tautulli.builders + tmdb.builders + trakttv.builders + tvdb.builders
dictionary_builders = [ dictionary_builders = [
@ -191,6 +206,9 @@ class CollectionBuilder:
self.missing_shows = [] self.missing_shows = []
self.methods = [] self.methods = []
self.filters = [] self.filters = []
self.rating_keys = []
self.missing_movies = []
self.missing_shows = []
self.posters = {} self.posters = {}
self.backgrounds = {} self.backgrounds = {}
self.summaries = {} self.summaries = {}
@ -1207,16 +1225,32 @@ class CollectionBuilder:
self.details["collection_mode"] = "hide" self.details["collection_mode"] = "hide"
self.sync = True self.sync = True
def run_methods(self, collection_obj, collection_name, rating_key_map, movie_map, show_map): try:
items_found = 0 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: for method, values in self.methods:
logger.debug("") logger.debug("")
logger.debug(f"Method: {method}") logger.debug(f"Method: {method}")
logger.debug(f"Values: {values}") logger.debug(f"Values: {values}")
for value in values: for value in values:
items = []
missing_movies = []
missing_shows = []
def check_map(input_ids): def check_map(input_ids):
movie_ids, show_ids = input_ids movie_ids, show_ids = input_ids
items_found_inside = 0 items_found_inside = 0
@ -1224,134 +1258,252 @@ class CollectionBuilder:
items_found_inside += len(movie_ids) items_found_inside += len(movie_ids)
for movie_id in movie_ids: for movie_id in movie_ids:
if movie_id in movie_map: if movie_id in movie_map:
items.extend(movie_map[movie_id]) add_rating_keys(movie_map[movie_id])
else: elif movie_id not in self.missing_movies:
missing_movies.append(movie_id) self.missing_movies.append(movie_id)
if len(show_ids) > 0: if len(show_ids) > 0:
items_found_inside += len(show_ids) items_found_inside += len(show_ids)
for show_id in show_ids: for show_id in show_ids:
if show_id in show_map: if show_id in show_map:
items.extend(show_map[show_id]) add_rating_keys(show_map[show_id])
else: elif show_id not in self.missing_shows:
missing_shows.append(show_id) self.missing_shows.append(show_id)
return items_found_inside return items_found_inside
logger.debug("") logger.debug("")
logger.debug(f"Value: {value}") logger.debug(f"Value: {value}")
logger.info("") logger.info("")
if "plex" in method: if "plex" in method: add_rating_keys(self.library.get_items(method, value))
items = self.library.get_items(method, value) elif "tautulli" in method: add_rating_keys(self.library.Tautulli.get_items(self.library, value))
items_found += len(items) elif "anidb" in method: check_map(self.config.AniDB.get_items(method, value, self.library.Plex.language))
elif "tautulli" in method: elif "anilist" in method: check_map(self.config.AniList.get_items(method, value))
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"]) elif "mal" in method: check_map(self.config.MyAnimeList.get_items(method, value))
items_found += len(items) elif "tvdb" in method: check_map(self.config.TVDb.get_items(method, value, self.library.Plex.language))
elif "anidb" in method: items_found += check_map(self.config.AniDB.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 "anilist" in method: items_found += check_map(self.config.AniList.get_items(method, value)) elif "letterboxd" in method: check_map(self.config.Letterboxd.get_items(method, value, self.library.Plex.language))
elif "mal" in method: items_found += check_map(self.config.MyAnimeList.get_items(method, value)) elif "tmdb" in method: check_map(self.config.TMDb.get_items(method, value, self.library.is_movie))
elif "tvdb" in method: items_found += check_map(self.config.TVDb.get_items(method, value, self.library.Plex.language)) elif "trakt" in method: check_map(self.config.Trakt.get_items(method, value, self.library.is_movie))
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))
else: logger.error(f"Collection Error: {method} method not supported") else: logger.error(f"Collection Error: {method} method not supported")
logger.info("") def add_to_collection(self, movie_map, show_map):
if len(items) > 0: name, collection_items = self.library.get_collection_name_and_items(self.obj if self.obj else self.name, self.smart_label_collection)
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) total = len(self.rating_keys)
else: max_length = len(str(total))
logger.error("No items found to add to this collection ") length = 0
for i, item in enumerate(self.rating_keys, 1):
if len(missing_movies) > 0 or len(missing_shows) > 0: try:
logger.info("") current = self.library.fetchItem(item.ratingKey if isinstance(item, (Movie, Show)) else int(item))
arr_filters = [] if not isinstance(current, (Movie, Show)):
for filter_method, filter_data in self.filters: raise NotFound
if (filter_method.startswith("original_language") and self.library.is_movie) or filter_method.startswith("tmdb_vote_count"): except (BadRequest, NotFound):
arr_filters.append((filter_method, filter_data)) logger.error(f"Plex Error: Item {item} not found")
if len(missing_movies) > 0: continue
missing_movies_with_names = [] match = True
for missing_id in missing_movies: if self.filters:
try: length = util.print_return(length, f"Filtering {(' ' * (max_length - len(str(i)))) + str(i)}/{total} {current.title}")
movie = self.config.TMDb.get_movie(missing_id) for filter_method, filter_data in self.filters:
except Failed as e: modifier = filter_method[-4:]
logger.error(e) method = filter_method[:-4] if modifier in [".not", ".lte", ".gte"] else filter_method
continue method_name = filter_alias[method] if method in filter_alias else method
match = True if method_name == "max_age":
for filter_method, filter_data in arr_filters: threshold_date = datetime.now() - timedelta(days=filter_data)
if (filter_method == "original_language" and movie.original_language not in filter_data) \ if current.originallyAvailableAt is None or current.originallyAvailableAt < threshold_date:
or (filter_method == "original_language.not" and movie.original_language in filter_data) \ match = False
or (filter_method == "tmdb_vote_count.gte" and movie.vote_count < filter_data) \ break
or (filter_method == "tmdb_vote_count.lte" and movie.vote_count > filter_data): elif method_name == "original_language":
match = False movie = None
break for key, value in movie_map.items():
if match: if current.ratingKey in value:
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: try:
self.library.Radarr.add_tmdb(missing_tmdb_ids, **self.radarr_options) movie = self.config.TMDb.get_movie(key)
except Failed as e: break
logger.error(e) except Failed:
if self.run_again: pass
self.missing_movies.extend(missing_tmdb_ids) if movie is None:
if len(missing_shows) > 0 and self.library.is_show: logger.warning(f"Filter Error: No TMDb ID found for {current.title}")
missing_shows_with_names = [] continue
for missing_id in missing_shows: if (modifier == ".not" and movie.original_language in filter_data) or (
try: modifier != ".not" and movie.original_language not in filter_data):
title = str(self.config.TVDb.get_series(self.library.Plex.language, missing_id).title.encode("ascii", "replace").decode()) match = False
except Failed as e: break
logger.error(e) elif method_name == "audio_track_title":
continue jailbreak = False
match = True for media in current.media:
if arr_filters: for part in media.parts:
show = self.config.TMDb.get_show(self.config.Convert.tvdb_to_tmdb(missing_id)) for audio in part.audioStreams():
for filter_method, filter_data in arr_filters: for check_title in filter_data:
if (filter_method == "tmdb_vote_count.gte" and show.vote_count < filter_data) \ title = audio.title if audio.title else ""
or (filter_method == "tmdb_vote_count.lte" and show.vote_count > filter_data): if check_title.lower() in title.lower():
match = False 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 break
if match: except Failed:
missing_shows_with_names.append((title, missing_id)) pass
if self.details["show_missing"] is True: if tmdb_item is None:
logger.info(f"{collection_name} Collection | ? | {title} (TVDB: {missing_id})") logger.warning(f"Filter Error: No TMDb ID found for {current.title}")
elif self.details["show_filtered"] is True: continue
logger.info(f"{collection_name} Collection | X | {title} (TVDb: {missing_id})") attr = tmdb_item.vote_count
logger.info(f"{len(missing_shows_with_names)} Show{'s' if len(missing_shows_with_names) > 1 else ''} Missing") else:
if self.details["save_missing"] is True: attr = getattr(current, method_name) / 60000 if method_name == "duration" else getattr(current, method_name)
self.library.add_missing(collection_name, missing_shows_with_names, False) if attr is None or (modifier == ".lte" and attr > filter_data) or (modifier == ".gte" and attr < filter_data):
if (self.add_to_sonarr and self.library.Sonarr) or self.run_again: match = False
missing_tvdb_ids = [missing_id for title, missing_id in missing_shows_with_names] break
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)
else: else:
self.library.query_data(item.removeCollection, collection_name) attrs = []
count_removed += 1 if method_name in ["video_resolution", "audio_language", "subtitle_language"]:
logger.info(f"{count_removed} {'Movie' if self.library.is_movie else 'Show'}{'s' if count_removed == 1 else ''} Removed") 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("") 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): def sync_collection(self):
if self.smart_url and self.smart_url != self.library.smart_filter(collection): logger.info("")
self.library.update_smart_collection(collection, self.smart_url) 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}") logger.info(f"Detail: Smart Filter updated to {self.smart_url}")
edits = {} edits = {}
@ -1379,50 +1531,50 @@ class CollectionBuilder:
elif "tmdb_show_details" in self.summaries: summary = get_summary("tmdb_show_details", self.summaries) elif "tmdb_show_details" in self.summaries: summary = get_summary("tmdb_show_details", self.summaries)
else: summary = None else: summary = None
if summary: if summary:
if str(summary) != str(collection.summary): if str(summary) != str(self.obj.summary):
edits["summary.value"] = summary edits["summary.value"] = summary
edits["summary.locked"] = 1 edits["summary.locked"] = 1
if "sort_title" in self.details: 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.value"] = self.details["sort_title"]
edits["titleSort.locked"] = 1 edits["titleSort.locked"] = 1
logger.info(f"Detail: sort_title updated Collection Sort Title to {self.details['sort_title']}") logger.info(f"Detail: sort_title updated Collection Sort Title to {self.details['sort_title']}")
if "content_rating" in self.details: 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.value"] = self.details["content_rating"]
edits["contentRating.locked"] = 1 edits["contentRating.locked"] = 1
logger.info(f"Detail: content_rating updated Collection Content Rating to {self.details['content_rating']}") logger.info(f"Detail: content_rating updated Collection Content Rating to {self.details['content_rating']}")
if "collection_mode" in self.details: if "collection_mode" in self.details:
if int(collection.collectionMode) not in plex.collection_mode_keys\ if int(self.obj.collectionMode) not in plex.collection_mode_keys\
or plex.collection_mode_keys[int(collection.collectionMode)] != self.details["collection_mode"]: or plex.collection_mode_keys[int(self.obj.collectionMode)] != self.details["collection_mode"]:
self.library.collection_mode_query(collection, 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']}") logger.info(f"Detail: collection_mode updated Collection Mode to {self.details['collection_mode']}")
if "collection_order" in self.details: if "collection_order" in self.details:
if int(collection.collectionSort) not in plex.collection_order_keys\ if int(self.obj.collectionSort) not in plex.collection_order_keys\
or plex.collection_order_keys[int(collection.collectionSort)] != self.details["collection_order"]: or plex.collection_order_keys[int(self.obj.collectionSort)] != self.details["collection_order"]:
self.library.collection_order_query(collection, 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']}") 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: 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"]) labels = util.get_list(self.details["label" if "label" in self.details else "label.sync"])
if "label.sync" in self.details: if "label.sync" in self.details:
for label in (la for la in item_labels if la not in labels): 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") logger.info(f"Detail: Label {label} removed")
for label in (la for la in labels if la not in item_labels): 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") logger.info(f"Detail: Label {label} added")
if len(self.item_details) > 0: if len(self.item_details) > 0:
labels = None labels = None
if "item_label" in self.item_details or "item_label.sync" in self.item_details: 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"]) 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: if labels is not None:
item_labels = [label.tag for label in item.labels] item_labels = [label.tag for label in item.labels]
if "item_label.sync" in self.item_details: if "item_label.sync" in self.item_details:
@ -1442,7 +1594,7 @@ class CollectionBuilder:
if len(edits) > 0: if len(edits) > 0:
logger.debug(edits) logger.debug(edits)
self.library.edit_query(collection, edits) self.library.edit_query(self.obj, edits)
logger.info("Details: have been updated") logger.info("Details: have been updated")
if self.library.asset_directory: if self.library.asset_directory:
@ -1466,13 +1618,13 @@ class CollectionBuilder:
matches = glob.glob(background_filter) matches = glob.glob(background_filter)
if len(matches) > 0: if len(matches) > 0:
self.backgrounds["asset_directory"] = os.path.abspath(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]) self.library.update_item_from_assets(item, dirs=[path])
def set_image(image_method, images, is_background=False): 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]}" message = f"{'background' if is_background else 'poster'} to [{'File' if image_method in image_file_details else 'URL'}] {images[image_method]}"
try: 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}") logger.info(f"Detail: {image_method} updated collection {message}")
except BadRequest: except BadRequest:
logger.error(f"Detail: {image_method} failed to update {message}") 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) elif "tmdb_show_details" in self.backgrounds: set_image("tmdb_show_details", self.backgrounds, is_background=True)
else: logger.info("No background to update") else: logger.info("No background to update")
def run_collections_again(self, collection_obj, movie_map, show_map): def run_collections_again(self, movie_map, show_map):
name, collection_items = self.library.get_collection_name_and_items(collection_obj, self.smart_label_collection) 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 = [] rating_keys = []
for mm in self.missing_movies: for mm in self.missing_movies:
if mm in movie_map: if mm in movie_map:

@ -1,7 +1,6 @@
import logging, os, random, sqlite3 import logging, os, random, sqlite3
from contextlib import closing from contextlib import closing
from datetime import datetime, timedelta from datetime import datetime, timedelta
from modules.util import Failed
logger = logging.getLogger("Plex Meta Manager") logger = logging.getLogger("Plex Meta Manager")

@ -7,7 +7,6 @@ from plexapi import utils
from plexapi.exceptions import BadRequest, NotFound, Unauthorized from plexapi.exceptions import BadRequest, NotFound, Unauthorized
from plexapi.collection import Collections from plexapi.collection import Collections
from plexapi.server import PlexServer from plexapi.server import PlexServer
from plexapi.video import Movie, Show
from retrying import retry from retrying import retry
from ruamel import yaml from ruamel import yaml
from urllib import parse from urllib import parse
@ -77,20 +76,6 @@ item_advance_keys = {
"item_use_original_title": ("useOriginalTitle", use_original_title_options) "item_use_original_title": ("useOriginalTitle", use_original_title_options)
} }
new_plex_agents = ["tv.plex.agents.movie", "tv.plex.agents.series"] 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 = [ searches = [
"title", "title.and", "title.not", "title.begins", "title.ends", "title", "title.and", "title.not", "title.begins", "title.ends",
"studio", "studio.and", "studio.not", "studio.begins", "studio.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) 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) @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex)
def exact_search(self, title, libtype=None): def exact_search(self, title, libtype=None, year=None):
return self.Plex.search(libtype=libtype, **{"title=": title}) 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) @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex)
def get_labeled_items(self, label): def get_labeled_items(self, label):
@ -652,125 +641,6 @@ class PlexAPI:
name = collection.title if isinstance(collection, Collections) else str(collection) name = collection.title if isinstance(collection, Collections) else str(collection)
return name, self.get_collection_items(collection, smart_label_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): def search_item(self, data, year=None):
kwargs = {} kwargs = {}
if year is not None: if year is not None:

@ -21,10 +21,11 @@ class TautulliAPI:
self.url = params["url"] self.url = params["url"]
self.apikey = params["apikey"] self.apikey = params["apikey"]
def get_items(self, library, time_range=30, stats_count=20, list_type="popular", stats_count_buffer=20): def get_items(self, library, params):
logger.info(f"Processing Tautulli Most {'Popular' if list_type == 'popular' else 'Watched'}: {stats_count} {'Movies' if library.is_movie else 'Shows'}") query_size = int(params["list_size"]) + int(params["list_buffer"])
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)}") logger.info(f"Processing Tautulli Most {params['list_type'].capitalize()}: {params['list_size']} {'Movies' if library.is_movie else 'Shows'}")
stat_id = f"{'popular' if list_type == 'popular' else 'top'}_{'movies' if library.is_movie else 'tv'}" 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 items = None
for entry in response["response"]["data"]: for entry in response["response"]["data"]:
@ -38,19 +39,17 @@ class TautulliAPI:
rating_keys = [] rating_keys = []
count = 0 count = 0
for item in items: for item in items:
if item["section_id"] == section_id and count < int(stats_count): if item["section_id"] == section_id and count < int(params['list_size']):
rk = None
try: try:
library.fetchItem(int(item["rating_key"])) library.fetchItem(int(item["rating_key"]))
rk = item["rating_key"] rating_keys.append(item["rating_key"])
except (BadRequest, NotFound): except (BadRequest, NotFound):
new_item = library.exact_search(item["title"]) new_item = library.exact_search(item["title"], year=item["year"])
if new_item: if new_item:
rk = new_item[0].ratingKey rating_keys.append(new_item[0].ratingKey)
else: else:
logger.error(f"Plex Error: Item {item} not found") logger.error(f"Plex Error: Item {item} not found")
continue continue
rating_keys.append(rk)
count += 1 count += 1
return rating_keys return rating_keys

@ -186,105 +186,37 @@ def update_libraries(config, is_test, requested_collections, resume_from):
util.separator(f"{builder.name} Collection") util.separator(f"{builder.name} Collection")
logger.info("") logger.info("")
try: try:
collection_obj = library.get_collection(builder.name) builder.run_collections_again(movie_map, show_map)
except Failed as e: except Failed as e:
util.print_stacktrace()
util.print_multiline(e, error=True) 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): def map_guids(config, library):
for mapping_name, collection_attrs in requested_collections.items(): movie_map = {}
if is_test and ("test" not in collection_attrs or collection_attrs["test"] is not True): show_map = {}
no_template_test = True length = 0
if "template" in collection_attrs and collection_attrs["template"]: logger.info(f"Mapping {'Movie' if library.is_movie else 'Show'} Library: {library.name}")
for data_template in util.get_list(collection_attrs["template"], split=False): items = library.Plex.all()
if "name" in data_template \ for i, item in enumerate(items, 1):
and data_template["name"] \ length = util.print_return(length, f"Processing: {i}/{len(items)} {item.title}")
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: try:
if resume_from and resume_from != mapping_name: id_type, main_id = config.Convert.get_id(item, library, length)
continue except BadRequest:
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:
util.print_stacktrace() util.print_stacktrace()
logger.error(f"Unknown Error: {e}") util.print_end(length, f"{'Cache | ! |' if config.Cache else 'Mapping Error:'} | {item.guid} for {item.title} not found")
return resume_from 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): def mass_metadata(config, library, movie_map, show_map):
length = 0 length = 0
@ -380,32 +312,70 @@ def mass_metadata(config, library, movie_map, show_map):
except Failed: except Failed:
pass pass
def map_guids(config, library): def run_collection(config, library, metadata, requested_collections, is_test, resume_from, movie_map, show_map):
movie_map = {} for mapping_name, collection_attrs in requested_collections.items():
show_map = {} if is_test and ("test" not in collection_attrs or collection_attrs["test"] is not True):
length = 0 no_template_test = True
logger.info(f"Mapping {'Movie' if library.is_movie else 'Show'} Library: {library.name}") if "template" in collection_attrs and collection_attrs["template"]:
items = library.Plex.all() for data_template in util.get_list(collection_attrs["template"], split=False):
for i, item in enumerate(items, 1): if "name" in data_template \
length = util.print_return(length, f"Processing: {i}/{len(items)} {item.title}") 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: try:
id_type, main_id = config.Convert.get_id(item, library, length) if resume_from and resume_from != mapping_name:
except BadRequest: 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_stacktrace()
util.print_end(length, f"{'Cache | ! |' if config.Cache else 'Mapping Error:'} | {item.guid} for {item.title} not found") util.print_multiline(e, error=True)
continue except Exception as e:
if not isinstance(main_id, list): util.print_stacktrace()
main_id = [main_id] logger.error(f"Unknown Error: {e}")
if id_type == "movie": return resume_from
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
try: try:
if run or test or collections or libraries or resume: if run or test or collections or libraries or resume:

Loading…
Cancel
Save