#226 Added smart_url and smart_label

This commit is contained in:
meisnate12 2021-05-02 00:22:48 -04:00
parent 8add37d156
commit c3a5a0cbca
3 changed files with 395 additions and 208 deletions

View file

@ -2,7 +2,6 @@ import glob, logging, os, re
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.collection import Collections
from plexapi.exceptions import BadRequest, NotFound
logger = logging.getLogger("Plex Meta Manager")
@ -82,6 +81,16 @@ numbered_builders = [
"trakt_watched",
"trakt_collected"
]
smart_collection_invalid = ["collection_mode", "collection_order"]
smart_url_collection_invalid = [
"item_label", "item_label.sync", "item_episode_sorting", "item_keep_episodes", "item_delete_episodes",
"item_season_display", "item_episode_ordering", "item_metadata_language", "item_use_original_title",
"run_again", "sync_mode", "show_filtered", "show_missing", "save_missing", "smart_label",
"radarr_add", "radarr_folder", "radarr_monitor", "radarr_availability",
"radarr_quality", "radarr_tag", "radarr_search",
"sonarr_add", "sonarr_folder", "sonarr_monitor", "sonarr_quality", "sonarr_language",
"sonarr_series", "sonarr_season", "sonarr_tag", "sonarr_search", "sonarr_cutoff_search"
]
all_details = [
"sort_title", "content_rating", "collection_mode", "collection_order",
"summary", "tmdb_summary", "tmdb_description", "tmdb_biography", "tvdb_summary",
@ -99,6 +108,8 @@ collectionless_details = [
"name_mapping", "label", "label_sync_mode", "test"
]
ignored_details = [
"smart_label",
"smart_url",
"run_again",
"schedule",
"sync_mode",
@ -342,8 +353,42 @@ class CollectionBuilder:
logger.info(f"Scanning {self.name} Collection")
self.collectionless = "plex_collectionless" in methods
self.run_again = "run_again" in methods
self.collectionless = "plex_collectionless" in methods
self.smart_sort = "title.asc"
self.smart_label_collection = False
if "smart_label" in methods:
self.smart_label_collection = True
if self.data[methods["smart_label"]]:
if str(self.data[methods["smart_label"]]).lower() in plex.smart_sorts:
self.smart_sort = str(self.data[methods["smart_label"]]).lower()
else:
logger.info("")
logger.warning(f"Collection Error: smart_label attribute: {self.data[methods['smart_label']]} is invalid defaulting to title.asc")
else:
logger.info("")
logger.warning("Collection Error: smart_label attribute is blank defaulting to title.asc")
self.smart_url = None
self.smart_url_collection = False
if "smart_url" in methods:
if self.data[methods["smart_url"]]:
self.smart_url_collection = True
try:
self.smart_url = library.get_smart_filter_from_uri(self.data[methods["smart_url"]])
except ValueError:
raise Failed("Collection Error: smart_url is incorrectly formatted")
else:
raise Failed("Collection Error: smart_url attribute is blank")
if self.smart_label_collection and self.collectionless:
raise Failed(f"Collection Error: plex_collectionless & smart_label_collection attributes cannot go together")
if self.smart_url_collection and self.run_again:
raise Failed(f"Collection Error: run_again & smart_url_collection attributes cannot go together")
self.smart = self.smart_url_collection or self.smart_label_collection
if "tmdb_person" in methods:
if self.data[methods["tmdb_person"]]:
@ -355,8 +400,10 @@ class CollectionBuilder:
self.summaries["tmdb_person"] = person.biography
if hasattr(person, "profile_path") and person.profile_path:
self.posters["tmdb_person"] = f"{config.TMDb.image_url}{person.profile_path}"
if len(valid_names) > 0: self.details["tmdb_person"] = valid_names
else: raise Failed(f"Collection Error: No valid TMDb Person IDs in {self.data[methods['tmdb_person']]}")
if len(valid_names) > 0:
self.details["tmdb_person"] = valid_names
else:
raise Failed(f"Collection Error: No valid TMDb Person IDs in {self.data[methods['tmdb_person']]}")
else:
raise Failed("Collection Error: tmdb_person attribute is blank")
@ -390,8 +437,14 @@ class CollectionBuilder:
raise Failed(f"Collection Error: {method_name} plex search only works for movie libraries")
elif method_name in plex.show_only_searches and self.library.is_movie:
raise Failed(f"Collection Error: {method_name} plex search only works for show libraries")
elif method_name in smart_collection_invalid and self.smart:
raise Failed(f"Collection Error: {method_name} attribute only works with normal collections")
elif method_name not in collectionless_details and self.collectionless:
raise Failed(f"Collection Error: {method_name} attribute does not work for Collectionless collection")
elif self.smart_url_collection and method_name in all_builders:
raise Failed(f"Collection Error: {method_name} builder not allowed when using smart_url")
elif self.smart_url_collection and method_name in smart_url_collection_invalid:
raise Failed(f"Collection Error: {method_name} detail not allowed when using smart_url")
elif method_name == "summary":
self.summaries[method_name] = method_data
elif method_name == "tmdb_summary":
@ -987,6 +1040,10 @@ class CollectionBuilder:
self.add_to_radarr = self.library.Radarr.add if self.library.Radarr else False
if self.add_to_sonarr is None:
self.add_to_sonarr = self.library.Sonarr.add if self.library.Sonarr else False
if self.smart_url_collection:
self.add_to_radarr = False
self.add_to_sonarr = False
if self.collectionless:
self.add_to_radarr = False
@ -1040,8 +1097,10 @@ class CollectionBuilder:
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")
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"], rating_key_map, movie_map, show_map)
else: logger.error("No items found to add to this collection ")
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("")
@ -1074,13 +1133,15 @@ class CollectionBuilder:
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:
try:
self.library.Radarr.add_tmdb([missing_id for title, missing_id in missing_movies_with_names], **self.radarr_options)
except Failed as e:
logger.error(e)
if self.run_again:
self.missing_movies.extend([missing_id for title, missing_id in missing_movies_with_names])
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:
@ -1106,13 +1167,15 @@ class CollectionBuilder:
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:
try:
self.library.Sonarr.add_tvdb([missing_id for title, missing_id in missing_shows_with_names], **self.sonarr_options)
except Failed as e:
logger.error(e)
if self.run_again:
self.missing_shows.extend([missing_id for title, missing_id in missing_shows_with_names])
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("")
@ -1120,12 +1183,19 @@ class CollectionBuilder:
for ratingKey, item in rating_key_map.items():
if item is not None:
logger.info(f"{collection_name} Collection | - | {item.title}")
item.removeCollection(collection_name)
if self.smart_label_collection:
self.library.query_data(item.removeLabel, collection_name)
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("")
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)
logger.info(f"Detail: Smart Filter updated to {self.smart_url}")
edits = {}
def get_summary(summary_method, summaries):
logger.info(f"Detail: {summary_method} updated Collection Summary")
@ -1151,51 +1221,58 @@ class CollectionBuilder:
elif "tmdb_show_details" in self.summaries: summary = get_summary("tmdb_show_details", self.summaries)
else: summary = None
if summary:
edits["summary.value"] = summary
edits["summary.locked"] = 1
if str(summary) != str(collection.summary):
edits["summary.value"] = summary
edits["summary.locked"] = 1
if "sort_title" in self.details:
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 str(self.details["sort_title"]) != str(collection.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:
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 str(self.details["content_rating"]) != str(collection.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:
collection.modeUpdate(mode=self.details["collection_mode"])
logger.info(f"Detail: collection_mode updated Collection Mode to {self.details['collection_mode']}")
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"])
logger.info(f"Detail: collection_mode updated Collection Mode to {self.details['collection_mode']}")
if "collection_order" in self.details:
collection.sortUpdate(sort=self.details["collection_order"])
logger.info(f"Detail: collection_order updated Collection Order to {self.details['collection_order']}")
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"])
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]
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):
collection.removeLabel(label)
self.library.query_data(collection.removeLabel, label)
logger.info(f"Detail: Label {label} removed")
for label in (la for la in labels if la not in item_labels):
collection.addLabel(label)
self.library.query_data(collection.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 collection.items():
for item in self.library.get_collection_items(collection, 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:
for label in (la for la in item_labels if la not in labels):
item.removeLabel(label)
self.library.query_data(item.removeLabel, label)
logger.info(f"Detail: Label {label} removed from {item.title}")
for label in (la for la in labels if la not in item_labels):
item.addLabel(label)
self.library.query_data(item.addLabel, label)
logger.info(f"Detail: Label {label} added to {item.title}")
advance_edits = {}
for method_name, method_data in self.item_details.items():
@ -1207,8 +1284,7 @@ class CollectionBuilder:
if len(edits) > 0:
logger.debug(edits)
collection.edit(**edits)
collection.reload()
self.library.collection_edit_query(collection, edits)
logger.info("Details: have been updated")
if self.library.asset_directory:
@ -1241,10 +1317,10 @@ class CollectionBuilder:
matches = glob.glob(os.path.join(path, folder, "background.*"))
background_path = os.path.abspath(matches[0]) if len(matches) > 0 else None
if poster_path:
item.uploadPoster(filepath=poster_path)
self.library.upload_image(item, poster_path, url=False)
logger.info(f"Detail: asset_directory updated {item.title}'s poster to [file] {poster_path}")
if background_path:
item.uploadArt(filepath=background_path)
self.library.upload_image(item, background_path, poster=False, url=False)
logger.info(f"Detail: asset_directory updated {item.title}'s background to [file] {background_path}")
if poster_path is None and background_path is None:
logger.warning(f"No Files Found: {os.path.join(path, folder)}")
@ -1253,13 +1329,13 @@ class CollectionBuilder:
matches = glob.glob(os.path.join(path, folder, f"Season{'0' if season.seasonNumber < 10 else ''}{season.seasonNumber}.*"))
if len(matches) > 0:
season_path = os.path.abspath(matches[0])
season.uploadPoster(filepath=season_path)
self.library.upload_image(season, season_path, url=False)
logger.info(f"Detail: asset_directory updated {item.title} Season {season.seasonNumber}'s poster to [file] {season_path}")
for episode in season.episodes():
matches = glob.glob(os.path.join(path, folder, f"{episode.seasonEpisode.upper()}.*"))
if len(matches) > 0:
episode_path = os.path.abspath(matches[0])
episode.uploadPoster(filepath=episode_path)
self.library.upload_image(episode, episode_path, url=False)
logger.info(f"Detail: asset_directory updated {item.title} {episode.seasonEpisode.upper()}'s poster to [file] {episode_path}")
else:
logger.warning(f"No Folder: {os.path.join(path, folder)}")
@ -1267,14 +1343,7 @@ class CollectionBuilder:
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:
if image_method in image_file_details and is_background:
collection.uploadArt(filepath=images[image_method])
elif image_method in image_file_details:
collection.uploadPoster(filepath=images[image_method])
elif is_background:
collection.uploadArt(url=images[image_method])
else:
collection.uploadPoster(url=images[image_method])
self.library.upload_image(collection, 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}")
@ -1327,8 +1396,7 @@ class CollectionBuilder:
else: logger.info("No background to update")
def run_collections_again(self, collection_obj, movie_map, show_map):
collection_items = collection_obj.items() if isinstance(collection_obj, Collections) else []
name = collection_obj.title if isinstance(collection_obj, Collections) else collection_obj
name, collection_items = self.library.get_collection_name_and_items(collection_obj, self.smart_label_collection)
rating_keys = []
for mm in self.missing_movies:
if mm in movie_map:
@ -1346,8 +1414,10 @@ class CollectionBuilder:
continue
if current in collection_items:
logger.info(f"{name} Collection | = | {current.title}")
elif self.smart_label_collection:
self.library.query_data(current.addLabel, name)
else:
current.addCollection(name)
self.library.query_data(current.addCollection, name)
logger.info(f"{name} Collection | + | {current.title}")
logger.info(f"{len(rating_keys)} {'Movie' if self.library.is_movie else 'Show'}{'s' if len(rating_keys) > 1 else ''} Processed")

View file

@ -479,133 +479,55 @@ class Config:
logger.warning(f"Collection: {resume_from} not in {library.name}")
continue
if collections:
for mapping_name, collection_attrs in collections.items():
if 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 library.templates \
and data_template["name"] in library.templates \
and library.templates[data_template["name"]] \
and "test" in library.templates[data_template["name"]] \
and library.templates[data_template["name"]]["test"] is True:
no_template_test = False
if no_template_test:
continue
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")
resume_from = self.run_collection(library, collections, test, resume_from, movie_map, show_map)
logger.info("")
util.separator(f"{mapping_name} Collection")
logger.info("")
rating_key_map = {}
try:
builder = CollectionBuilder(self, library, mapping_name, collection_attrs)
except Failed as ef:
util.print_multiline(ef, error=True)
continue
except Exception as ee:
util.print_stacktrace()
logger.error(ee)
continue
try:
collection_obj = library.get_collection(mapping_name)
collection_name = collection_obj.title
except Failed:
collection_obj = None
collection_name = mapping_name
if len(builder.schedule) > 0:
util.print_multiline(builder.schedule, info=True)
logger.info("")
if builder.sync:
logger.info("Sync Mode: sync")
if collection_obj:
for item in collection_obj.items():
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]}")
builder.run_methods(collection_obj, collection_name, rating_key_map, movie_map, show_map)
try:
plex_collection = library.get_collection(collection_name)
except Failed as e:
logger.debug(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()
logger.error(f"Unknown Error: {e}")
if library.assets_for_all is True and not test and not requested_collections:
logger.info("")
util.separator(f"All {'Movies' if library.is_movie else 'Shows'} Assets Check for {library.name} Library")
logger.info("")
for item in library.get_all():
folder = os.path.basename(os.path.dirname(item.locations[0]) if library.is_movie else item.locations[0])
for ad in library.asset_directory:
if library.asset_folders:
poster_path = os.path.join(ad, folder, "poster.*")
else:
poster_path = os.path.join(ad, f"{folder}.*")
matches = glob.glob(poster_path)
if len(matches) > 0:
item.uploadPoster(filepath=os.path.abspath(matches[0]))
logger.info(f"Detail: asset_directory updated {item.title}'s poster to [file] {os.path.abspath(matches[0])}")
if library.asset_folders:
matches = glob.glob(os.path.join(ad, folder, "background.*"))
if len(matches) > 0:
item.uploadArt(filepath=os.path.abspath(matches[0]))
logger.info(f"Detail: asset_directory updated {item.title}'s background to [file] {os.path.abspath(matches[0])}")
if library.is_show:
for season in item.seasons():
matches = glob.glob(os.path.join(ad, folder, f"Season{'0' if season.seasonNumber < 10 else ''}{season.seasonNumber}.*"))
if len(matches) > 0:
season_path = os.path.abspath(matches[0])
season.uploadPoster(filepath=season_path)
logger.info(f"Detail: asset_directory updated {item.title} Season {season.seasonNumber}'s poster to [file] {season_path}")
for episode in season.episodes():
matches = glob.glob(os.path.join(ad, folder, f"{episode.seasonEpisode.upper()}.*"))
if len(matches) > 0:
episode_path = os.path.abspath(matches[0])
episode.uploadPoster(filepath=episode_path)
logger.info(f"Detail: asset_directory updated {item.title} {episode.seasonEpisode.upper()}'s poster to [file] {episode_path}")
if library.show_unmanaged is True and not test and not requested_collections:
logger.info("")
util.separator(f"Unmanaged Collections in {library.name} Library")
logger.info("")
unmanaged_count = 0
collections_in_plex = [str(plex_col) for plex_col in collections]
for col in library.get_all_collections():
if col.title not in collections_in_plex:
logger.info(col.title)
unmanaged_count += 1
logger.info("{} Unmanaged Collections".format(unmanaged_count))
else:
if library.show_unmanaged is True and not test and not requested_collections:
logger.info("")
logger.error("No collection to update")
util.separator(f"Unmanaged Collections in {library.name} Library")
logger.info("")
unmanaged_count = 0
collections_in_plex = [str(plex_col) for plex_col in collections]
for col in library.get_all_collections():
if col.title not in collections_in_plex:
logger.info(col.title)
unmanaged_count += 1
logger.info("{} Unmanaged Collections".format(unmanaged_count))
if library.assets_for_all is True and not test and not requested_collections:
logger.info("")
util.separator(f"All {'Movies' if library.is_movie else 'Shows'} Assets Check for {library.name} Library")
logger.info("")
for item in library.get_all():
folder = os.path.basename(os.path.dirname(item.locations[0]) if library.is_movie else item.locations[0])
for ad in library.asset_directory:
if library.asset_folders:
if not os.path.isdir(os.path.join(ad, folder)):
continue
poster_path = os.path.join(ad, folder, "poster.*")
else:
poster_path = os.path.join(ad, f"{folder}.*")
matches = glob.glob(poster_path)
if len(matches) > 0:
library.upload_image(item, os.path.abspath(matches[0]), url=False)
logger.info(f"Detail: asset_directory updated {item.title}'s poster to [file] {os.path.abspath(matches[0])}")
if library.asset_folders:
matches = glob.glob(os.path.join(ad, folder, "background.*"))
if len(matches) > 0:
library.upload_image(item, os.path.abspath(matches[0]), poster=False, url=False)
logger.info(f"Detail: asset_directory updated {item.title}'s background to [file] {os.path.abspath(matches[0])}")
if library.is_show:
for season in item.seasons():
matches = glob.glob(os.path.join(ad, folder, f"Season{'0' if season.seasonNumber < 10 else ''}{season.seasonNumber}.*"))
if len(matches) > 0:
season_path = os.path.abspath(matches[0])
library.upload_image(season, season_path, url=False)
logger.info(f"Detail: asset_directory updated {item.title} Season {season.seasonNumber}'s poster to [file] {season_path}")
for episode in season.episodes():
matches = glob.glob(os.path.join(ad, folder, f"{episode.seasonEpisode.upper()}.*"))
if len(matches) > 0:
episode_path = os.path.abspath(matches[0])
library.upload_image(episode, episode_path, url=False)
logger.info(f"Detail: asset_directory updated {item.title} {episode.seasonEpisode.upper()}'s poster to [file] {episode_path}")
has_run_again = False
for library in self.libraries:
@ -645,6 +567,99 @@ class Config:
continue
builder.run_collections_again(collection_obj, movie_map, show_map)
def run_collection(self, library, collections, test, resume_from, movie_map, show_map):
for mapping_name, collection_attrs in collections.items():
if 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 library.templates \
and data_template["name"] in library.templates \
and library.templates[data_template["name"]] \
and "test" in library.templates[data_template["name"]] \
and library.templates[data_template["name"]]["test"] is True:
no_template_test = False
if no_template_test:
continue
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(self, library, mapping_name, collection_attrs)
except Failed as f:
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_collection:
builder.run_methods(collection_obj, collection_name, rating_key_map, movie_map, show_map)
try:
if not collection_obj and builder.smart_url_collection:
library.create_smart_collection(collection_name, 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()
logger.error(f"Unknown Error: {e}")
return resume_from
def mass_metadata(self, library, movie_map, show_map):
length = 0
logger.info("")
@ -695,10 +710,10 @@ class Config:
item_genres = [genre.tag for genre in item.genres]
display_str = ""
for genre in (g for g in item_genres if g not in new_genres):
item.removeGenre(genre)
library.query_data(item.removeGenre, genre)
display_str += f"{', ' if len(display_str) > 0 else ''}-{genre}"
for genre in (g for g in new_genres if g not in item_genres):
item.addGenre(genre)
library.query_data(item.addGenre, genre)
display_str += f"{', ' if len(display_str) > 0 else ''}+{genre}"
if len(display_str) > 0:
util.print_end(length, f"{item.title[:25]:<25} | Genres | {display_str}")

View file

@ -2,12 +2,14 @@ import logging, os, re, requests
from datetime import datetime, timedelta
from modules import util
from modules.util import Failed
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
logger = logging.getLogger("Plex Meta Manager")
@ -34,6 +36,8 @@ plex_languages = ["default", "ar-SA", "ca-ES", "cs-CZ", "da-DK", "de-DE", "el-GR
metadata_language_options = {lang.lower(): lang for lang in plex_languages}
metadata_language_options["default"] = None
use_original_title_options = {"default": -1, "no": 0, "yes": 1}
collection_mode_keys = {-1: "default", 0: "hide", 1: "hideItems", 2: "showItems"}
collection_order_keys = {0: "release", 1: "alpha", 2: "custom"}
advance_keys = {
"episode_sorting": ("episodeSort", episode_sorting_options),
"keep_episodes": ("autoDeletionItemPolicyUnwatchedLibrary", keep_episodes_options),
@ -108,6 +112,19 @@ tmdb_searches = [
"producer", "producer.and", "producer.not",
"writer", "writer.and", "writer.not"
]
smart_sorts = {
"title.asc": "titleSort", "title.desc": "titleSort%3Adesc",
"year.asc": "year", "year.desc": "year%3Adesc",
"originally_available.asc": "originallyAvailableAt", "originally_available.desc": "originallyAvailableAt%3Adesc",
"critic_rating.asc": "rating", "critic_rating.desc": "rating%3Adesc",
"audience_rating.asc": "audienceRating", "audience_rating.desc": "audienceRating%3Adesc",
"user_rating.asc": "userRating", "user_rating.desc": "userRating%3Adesc",
"content_rating.asc": "contentRating", "content_rating.desc": "contentRating%3Adesc",
"duration.asc": "duration", "duration.desc": "duration%3Adesc",
"plays.asc": "viewCount", "plays.desc": "viewCount%3Adesc",
"added.asc": "addedAt", "added.desc": "addedAt%3Adesc",
"random": "random"
}
sorts = {
None: None,
"title.asc": "titleSort:asc", "title.desc": "titleSort:desc",
@ -174,7 +191,7 @@ class PlexAPI:
self.collections = get_dict("collections")
if self.metadata is None and self.collections is None:
raise Failed("YAML Error: metadata attributes or collections attribute required")
raise Failed("YAML Error: metadata or collections attribute is required")
if params["asset_directory"]:
for ad in params["asset_directory"]:
@ -211,6 +228,10 @@ class PlexAPI:
def search(self, title=None, libtype=None, sort=None, maxresults=None, **kwargs):
return self.Plex.search(title=title, sort=sort, maxresults=maxresults, libtype=libtype, **kwargs)
@retry(stop_max_attempt_number=6, wait_fixed=10000)
def get_labeled_items(self, label):
return self.Plex.search(label=label)
@retry(stop_max_attempt_number=6, wait_fixed=10000)
def fetchItem(self, data):
return self.PlexServer.fetchItem(data)
@ -224,8 +245,36 @@ class PlexAPI:
return self.PlexServer.search(data)
@retry(stop_max_attempt_number=6, wait_fixed=10000)
def add_collection(self, item, name):
item.addCollection(name)
def query(self, method):
return method()
@retry(stop_max_attempt_number=6, wait_fixed=10000)
def query_data(self, method, data):
return method(data)
@retry(stop_max_attempt_number=6, wait_fixed=10000)
def collection_mode_query(self, collection, data):
collection.modeUpdate(mode=data)
@retry(stop_max_attempt_number=6, wait_fixed=10000)
def collection_order_query(self, collection, data):
collection.sortUpdate(sort=data)
@retry(stop_max_attempt_number=6, wait_fixed=10000)
def collection_edit_query(self, collection, data):
collection.edit(**data)
collection.reload()
@retry(stop_max_attempt_number=6, wait_fixed=10000)
def upload_image(self, item, location, poster=True, url=True):
if poster and url:
item.uploadPoster(url=location)
elif poster:
item.uploadPoster(filepath=location)
elif url:
item.uploadArt(url=location)
else:
item.uploadArt(filepath=location)
@retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed)
def get_search_choices(self, search_name):
@ -239,8 +288,49 @@ class PlexAPI:
raise Failed(f"Collection Error: plex search attribute: {search_name} only supported with Plex's New TV Agent")
@retry(stop_max_attempt_number=6, wait_fixed=10000)
def refresh_item(self, rating_key):
requests.put(f"{self.url}/library/metadata/{rating_key}/refresh?X-Plex-Token={self.token}")
def get_labels(self):
return {label.title: label.key for label in self.Plex.listFilterChoices(field="label")}
@retry(stop_max_attempt_number=6, wait_fixed=10000)
def _query(self, key, post=False, put=False):
if post: method = self.Plex._server._session.post
elif put: method = self.Plex._server._session.put
else: method = None
self.Plex._server.query(key, method=method)
def create_smart_labels(self, title, sort):
labels = self.get_labels()
if title not in labels:
raise Failed(f"Plex Error: Label: {title} does not exist")
uri_args = f"?type=1&sort={smart_sorts[sort]}&label={labels[title]}"
self.create_smart_collection(title, uri_args)
def create_smart_collection(self, title, uri_args):
args = {
"type": 1,
"title": title,
"smart": 1,
"sectionId": self.Plex.key,
"uri": self.build_smart_filter(uri_args)
}
self._query(f"/library/collections{utils.joinArgs(args)}", post=True)
def get_smart_filter_from_uri(self, uri):
smart_filter = parse.parse_qs(parse.urlparse(uri.replace("/#!/", "/")).query)["key"][0]
return self.build_smart_filter(smart_filter[smart_filter.index("?"):])
def build_smart_filter(self, uri_args):
return f"server://{self.PlexServer.machineIdentifier}/com.plexapp.plugins.library/library/sections/{self.Plex.key}/all{uri_args}"
def update_smart_collection(self, collection, uri_args):
self._query(f"/library/collections/{collection.ratingKey}/items{utils.joinArgs({'uri': self.build_smart_filter(uri_args)})}", put=True)
def smart(self, collection):
return utils.cast(bool, self.get_collection(collection)._data.attrib.get('smart', '0'))
def smart_filter(self, collection):
smart_filter = self.get_collection(collection)._data.attrib.get('content')
return smart_filter[smart_filter.index("?"):]
def validate_search_list(self, data, search_name):
final_search = search_translation[search_name] if search_name in search_translation else search_name
@ -254,9 +344,16 @@ class PlexAPI:
return valid_list
def get_collection(self, data):
collection = util.choose_from_list(self.search(title=str(data), libtype="collection"), "collection", str(data), exact=True)
if collection: return collection
else: raise Failed(f"Plex Error: Collection {data} not found")
if isinstance(data, int):
collection = self.fetchItem(data)
elif isinstance(data, Collections):
collection = data
else:
collection = util.choose_from_list(self.search(title=str(data), libtype="collection"), "collection", str(data), exact=True)
if collection:
return collection
else:
raise Failed(f"Plex Error: Collection {data} not found")
def validate_collections(self, collections):
valid_collections = []
@ -384,9 +481,20 @@ class PlexAPI:
except yaml.scanner.ScannerError as e:
logger.error(f"YAML Error: {util.tab_new_lines(e)}")
def add_to_collection(self, collection, items, filters, show_filtered, rating_key_map, movie_map, show_map):
name = collection.title if isinstance(collection, Collections) else collection
collection_items = collection.items() if isinstance(collection, Collections) else []
def get_collection_items(self, collection, smart_label_collection):
if smart_label_collection:
return self.get_labeled_items(collection.title if isinstance(collection, Collections) else str(collection))
elif isinstance(collection, Collections):
return self.query(collection.items)
else:
return []
def get_collection_name_and_items(self, collection, smart_label_collection):
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
@ -496,7 +604,8 @@ class PlexAPI:
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
else: self.add_collection(current, name)
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 ''}"
@ -519,7 +628,7 @@ class PlexAPI:
item.edit(**edits)
item.reload()
if advanced and "languageOverride" in edits:
self.refresh_item(item.ratingKey)
self.query(item.refresh)
logger.info(f"{item_type}: {name}{' Advanced' if advanced else ''} Details Update Successful")
except BadRequest:
util.print_stacktrace()
@ -607,17 +716,10 @@ class PlexAPI:
else:
logger.error(f"Metadata Error: {attr} attribute is blank")
def set_image(attr, obj, group, alias, is_background=False):
def set_image(attr, obj, group, alias, poster=True, url=True):
if group[alias[attr]]:
message = f"{'background' if is_background else 'poster'} to [{'File' if attr.startswith('file') else 'URL'}] {group[alias[attr]]}"
if group[alias[attr]] and attr.startswith("url") and is_background:
obj.uploadArt(url=group[alias[attr]])
elif group[alias[attr]] and attr.startswith("url"):
obj.uploadPoster(url=group[alias[attr]])
elif group[alias[attr]] and attr.startswith("file") and is_background:
obj.uploadArt(filepath=group[alias[attr]])
elif group[alias[attr]] and attr.startswith("file"):
obj.uploadPoster(filepath=group[alias[attr]])
message = f"{'poster' if poster else 'background'} to [{'URL' if url else 'File'}] {group[alias[attr]]}"
self.upload_image(obj, group[alias[attr]], poster=poster, url=url)
logger.info(f"Detail: {attr} updated {message}")
else:
logger.error(f"Metadata Error: {attr} attribute is blank")
@ -626,11 +728,11 @@ class PlexAPI:
if "url_poster" in alias:
set_image("url_poster", obj, group, alias)
elif "file_poster" in alias:
set_image("file_poster", obj, group, alias)
set_image("file_poster", obj, group, alias, url=False)
if "url_background" in alias:
set_image("url_background", obj, group, alias, is_background=True)
set_image("url_background", obj, group, alias, poster=False)
elif "file_background" in alias:
set_image("file_background", obj, group, alias, is_background=True)
set_image("file_background", obj, group, alias, poster=False, url=False)
logger.info("")
util.separator()