mirror of
https://github.com/meisnate12/Plex-Meta-Manager
synced 2024-11-10 06:54:21 +00:00
#226 Added smart_url
and smart_label
This commit is contained in:
parent
8add37d156
commit
c3a5a0cbca
3 changed files with 395 additions and 208 deletions
|
@ -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":
|
||||
|
@ -988,6 +1041,10 @@ class CollectionBuilder:
|
|||
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
|
||||
self.add_to_sonarr = 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) 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_id for title, missing_id in missing_movies_with_names], **self.radarr_options)
|
||||
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_id for title, missing_id in missing_movies_with_names])
|
||||
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) 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_id for title, missing_id in missing_shows_with_names], **self.sonarr_options)
|
||||
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_id for title, missing_id in missing_shows_with_names])
|
||||
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,25 +1221,32 @@ 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):
|
||||
edits["summary.value"] = summary
|
||||
edits["summary.locked"] = 1
|
||||
|
||||
if "sort_title" in self.details:
|
||||
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:
|
||||
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"])
|
||||
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"])
|
||||
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:
|
||||
|
@ -1177,25 +1254,25 @@ class CollectionBuilder:
|
|||
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")
|
||||
|
||||
|
|
|
@ -479,118 +479,7 @@ 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")
|
||||
|
||||
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}")
|
||||
resume_from = self.run_collection(library, collections, test, resume_from, movie_map, show_map)
|
||||
|
||||
if library.show_unmanaged is True and not test and not requested_collections:
|
||||
logger.info("")
|
||||
|
@ -603,9 +492,42 @@ class Config:
|
|||
logger.info(col.title)
|
||||
unmanaged_count += 1
|
||||
logger.info("{} Unmanaged Collections".format(unmanaged_count))
|
||||
else:
|
||||
|
||||
if library.assets_for_all is True and not test and not requested_collections:
|
||||
logger.info("")
|
||||
logger.error("No collection to update")
|
||||
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}")
|
||||
|
|
152
modules/plex.py
152
modules/plex.py
|
@ -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):
|
||||
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")
|
||||
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()
|
||||
|
|
Loading…
Reference in a new issue