mirror of
https://github.com/meisnate12/Plex-Meta-Manager
synced 2024-11-10 06:54:21 +00:00
$484 add playlists
This commit is contained in:
parent
03b02a2575
commit
a5a27d25da
10 changed files with 559 additions and 119 deletions
|
@ -85,7 +85,7 @@ boolean_details = [
|
|||
string_details = ["sort_title", "content_rating", "name_mapping"]
|
||||
ignored_details = [
|
||||
"smart_filter", "smart_label", "smart_url", "run_again", "schedule", "sync_mode", "template", "test", "delete_not_scheduled",
|
||||
"tmdb_person", "build_collection", "collection_order", "collection_level", "validate_builders", "collection_name", "sort_by"
|
||||
"tmdb_person", "build_collection", "collection_order", "collection_level", "validate_builders", "collection_name", "sort_by", "libraries"
|
||||
]
|
||||
details = ["ignore_ids", "ignore_imdb_ids", "server_preroll", "collection_changes_webhooks", "collection_mode",
|
||||
"collection_minimum", "label"] + boolean_details + string_details
|
||||
|
@ -165,6 +165,11 @@ custom_sort_builders = [
|
|||
"mal_all", "mal_airing", "mal_upcoming", "mal_tv", "mal_movie", "mal_ova", "mal_special",
|
||||
"mal_popular", "mal_favorite", "mal_suggested", "mal_userlist", "mal_season", "mal_genre", "mal_studio"
|
||||
]
|
||||
playlist_attributes = [
|
||||
"playlist_name", "filters", "name_mapping", "show_filtered", "show_missing", "save_missing",
|
||||
"missing_only_released", "only_filter_missing", "delete_below_minimum", "ignore_ids", "ignore_imdb_ids",
|
||||
"server_preroll", "collection_changes_webhooks", "collection_minimum"
|
||||
] + custom_sort_builders + summary_details + poster_details + radarr_details + sonarr_details
|
||||
|
||||
class CollectionBuilder:
|
||||
def __init__(self, config, library, metadata, name, no_missing, data, playlist=False):
|
||||
|
@ -197,7 +202,7 @@ class CollectionBuilder:
|
|||
self.builders = []
|
||||
self.filters = []
|
||||
self.tmdb_filters = []
|
||||
self.rating_keys = []
|
||||
self.added_items = []
|
||||
self.filtered_keys = {}
|
||||
self.run_again_movies = []
|
||||
self.run_again_shows = []
|
||||
|
@ -460,7 +465,7 @@ class CollectionBuilder:
|
|||
suffix = f" and could not be found to delete"
|
||||
raise NotScheduled(f"{self.schedule}\n\nCollection {self.name} not scheduled to run{suffix}")
|
||||
|
||||
self.collectionless = "plex_collectionless" in methods
|
||||
self.collectionless = "plex_collectionless" in methods and not self.playlist
|
||||
|
||||
self.validate_builders = True
|
||||
if "validate_builders" in methods:
|
||||
|
@ -477,7 +482,7 @@ class CollectionBuilder:
|
|||
self.run_again = self._parse("run_again", self.data, datatype="bool", methods=methods, default=False)
|
||||
|
||||
self.build_collection = True
|
||||
if "build_collection" in methods:
|
||||
if "build_collection" in methods and not self.playlist:
|
||||
logger.debug("")
|
||||
logger.debug("Validating Method: build_collection")
|
||||
logger.debug(f"Value: {data[methods['build_collection']]}")
|
||||
|
@ -496,8 +501,8 @@ class CollectionBuilder:
|
|||
else:
|
||||
self.sync = self.data[methods["sync_mode"]].lower() == "sync"
|
||||
|
||||
self.custom_sort = False
|
||||
if "collection_order" in methods:
|
||||
self.custom_sort = self.playlist
|
||||
if "collection_order" in methods and not self.playlist:
|
||||
logger.debug("")
|
||||
logger.debug("Validating Method: collection_order")
|
||||
if self.data[methods["collection_order"]] is None:
|
||||
|
@ -512,7 +517,7 @@ class CollectionBuilder:
|
|||
raise Failed(f"{self.Type} Error: {self.data[methods['collection_order']]} collection_order invalid\n\trelease (Order Collection by release dates)\n\talpha (Order Collection Alphabetically)\n\tcustom (Custom Order Collection)")
|
||||
|
||||
self.sort_by = None
|
||||
if "sort_by" in methods:
|
||||
if "sort_by" in methods and not self.playlist:
|
||||
logger.debug("")
|
||||
logger.debug("Validating Method: sort_by")
|
||||
if self.data[methods["sort_by"]] is None:
|
||||
|
@ -525,7 +530,9 @@ class CollectionBuilder:
|
|||
self.sort_by = self.data[methods["sort_by"]]
|
||||
|
||||
self.collection_level = "movie" if self.library.is_movie else "show"
|
||||
if "collection_level" in methods:
|
||||
if self.playlist:
|
||||
self.collection_level = "item"
|
||||
if "collection_level" in methods and not self.playlist:
|
||||
logger.debug("")
|
||||
logger.debug("Validating Method: collection_level")
|
||||
if self.library.is_movie:
|
||||
|
@ -562,7 +569,7 @@ class CollectionBuilder:
|
|||
|
||||
self.smart_sort = "random"
|
||||
self.smart_label_collection = False
|
||||
if "smart_label" in methods:
|
||||
if "smart_label" in methods and not self.playlist:
|
||||
logger.debug("")
|
||||
logger.debug("Validating Method: smart_label")
|
||||
self.smart_label_collection = True
|
||||
|
@ -579,7 +586,7 @@ class CollectionBuilder:
|
|||
self.smart_url = None
|
||||
self.smart_type_key = None
|
||||
self.smart_filter_details = ""
|
||||
if "smart_url" in methods:
|
||||
if "smart_url" in methods and not self.playlist:
|
||||
logger.debug("")
|
||||
logger.debug("Validating Method: smart_url")
|
||||
if not self.data[methods["smart_url"]]:
|
||||
|
@ -591,7 +598,7 @@ class CollectionBuilder:
|
|||
except ValueError:
|
||||
raise Failed(f"{self.Type} Error: smart_url is incorrectly formatted")
|
||||
|
||||
if "smart_filter" in methods:
|
||||
if "smart_filter" in methods and not self.playlist:
|
||||
self.smart_type_key, self.smart_filter_details, self.smart_url = self.build_filter("smart_filter", self.data[methods["smart_filter"]], smart=True)
|
||||
|
||||
def cant_interact(attr1, attr2, fail=False):
|
||||
|
@ -626,6 +633,7 @@ class CollectionBuilder:
|
|||
try:
|
||||
if method_data is None and method_name in all_builders + plex.searches: raise Failed(f"{self.Type} Error: {method_final} attribute is blank")
|
||||
elif method_data is None and method_final not in none_details: logger.warning(f"Collection Warning: {method_final} attribute is blank")
|
||||
elif self.playlist and method_name not in playlist_attributes: raise Failed(f"{self.Type} Error: {method_final} attribute not allowed when using playlists")
|
||||
elif not self.config.Trakt and "trakt" in method_name: raise Failed(f"{self.Type} Error: {method_final} requires Trakt to be configured")
|
||||
elif not self.library.Radarr and "radarr" in method_name: raise Failed(f"{self.Type} Error: {method_final} requires Radarr to be configured")
|
||||
elif not self.library.Sonarr and "sonarr" in method_name: raise Failed(f"{self.Type} Error: {method_final} requires Sonarr to be configured")
|
||||
|
@ -703,7 +711,7 @@ class CollectionBuilder:
|
|||
|
||||
if self.build_collection:
|
||||
try:
|
||||
self.obj = self.library.get_collection(self.name)
|
||||
self.obj = self.library.get_playlist(self.name) if self.playlist else self.library.get_collection(self.name)
|
||||
if (self.smart and not self.obj.smart) or (not self.smart and self.obj.smart):
|
||||
logger.info("")
|
||||
logger.error(f"{self.Type} Error: Converting {self.obj.title} to a {'smart' if self.smart else 'normal'} collection")
|
||||
|
@ -1442,14 +1450,14 @@ class CollectionBuilder:
|
|||
if not isinstance(item, (Movie, Show, Season, Episode)):
|
||||
logger.error(f"{self.Type} Error: Item: {item} must be Movie, Show, Season, or Episode")
|
||||
continue
|
||||
if item.ratingKey not in self.rating_keys:
|
||||
if item not in self.added_items:
|
||||
if item.ratingKey in self.filtered_keys:
|
||||
if self.details["show_filtered"] is True:
|
||||
logger.info(f"{name} {self.Type} | X | {self.filtered_keys[item.ratingKey]}")
|
||||
else:
|
||||
current_title = self.item_title(item)
|
||||
current_title = util.item_title(item)
|
||||
if self.check_filters(item, f"{(' ' * (max_length - len(str(i))))}{i}/{total}"):
|
||||
self.rating_keys.append(item.ratingKey)
|
||||
self.added_items.append(item)
|
||||
else:
|
||||
self.filtered_keys[item.ratingKey] = current_title
|
||||
if self.details["show_filtered"] is True:
|
||||
|
@ -1724,29 +1732,34 @@ class CollectionBuilder:
|
|||
|
||||
def add_to_collection(self):
|
||||
name, collection_items = self.library.get_collection_name_and_items(self.obj if self.obj else self.name, self.smart_label_collection)
|
||||
total = len(self.rating_keys)
|
||||
total = len(self.added_items)
|
||||
amount_added = 0
|
||||
for i, item in enumerate(self.rating_keys, 1):
|
||||
try:
|
||||
current = self.fetch_item(item)
|
||||
except Failed as e:
|
||||
logger.error(e)
|
||||
continue
|
||||
current_operation = "=" if current in collection_items else "+"
|
||||
logger.info(util.adjust_space(f"{name} Collection | {current_operation} | {self.item_title(current)}"))
|
||||
if current in collection_items:
|
||||
self.plex_map[current.ratingKey] = None
|
||||
playlist_adds = []
|
||||
for item in self.added_items:
|
||||
current_operation = "=" if item in collection_items else "+"
|
||||
logger.info(util.adjust_space(f"{name} {self.Type} | {current_operation} | {util.item_title(item)}"))
|
||||
if item in collection_items:
|
||||
self.plex_map[item.ratingKey] = None
|
||||
else:
|
||||
self.library.alter_collection(current, name, smart_label_collection=self.smart_label_collection)
|
||||
if self.playlist:
|
||||
playlist_adds.append(item)
|
||||
else:
|
||||
self.library.alter_collection(item, name, smart_label_collection=self.smart_label_collection)
|
||||
amount_added += 1
|
||||
if self.details["collection_changes_webhooks"]:
|
||||
if self.library.is_movie and current.ratingKey in self.library.movie_rating_key_map:
|
||||
add_id = self.library.movie_rating_key_map[current.ratingKey]
|
||||
elif self.library.is_show and current.ratingKey in self.library.show_rating_key_map:
|
||||
add_id = self.library.show_rating_key_map[current.ratingKey]
|
||||
if self.library.is_movie and item.ratingKey in self.library.movie_rating_key_map:
|
||||
add_id = self.library.movie_rating_key_map[item.ratingKey]
|
||||
elif self.library.is_show and item.ratingKey in self.library.show_rating_key_map:
|
||||
add_id = self.library.show_rating_key_map[item.ratingKey]
|
||||
else:
|
||||
add_id = None
|
||||
self.notification_additions.append({"title": current.title, "id": add_id})
|
||||
self.notification_additions.append({"title": item.title, "id": add_id})
|
||||
if self.playlist and playlist_adds and not self.obj:
|
||||
self.obj = self.library.create_playlist(self.name, playlist_adds)
|
||||
logger.info("")
|
||||
logger.info(f"Playlist: {self.name} created")
|
||||
elif self.playlist and playlist_adds:
|
||||
self.obj.addItems(playlist_adds)
|
||||
util.print_end()
|
||||
logger.info("")
|
||||
logger.info(f"{total} {self.collection_level.capitalize()}{'s' if total > 1 else ''} Processed")
|
||||
|
@ -1754,15 +1767,20 @@ class CollectionBuilder:
|
|||
|
||||
def sync_collection(self):
|
||||
amount_removed = 0
|
||||
playlist_removes = []
|
||||
for ratingKey, item in self.plex_map.items():
|
||||
if item is not None:
|
||||
if amount_removed == 0:
|
||||
logger.info("")
|
||||
util.separator(f"Removed from {self.name} Collection", space=False, border=False)
|
||||
util.separator(f"Removed from {self.name} {self.Type}", space=False, border=False)
|
||||
logger.info("")
|
||||
self.library.reload(item)
|
||||
logger.info(f"{self.name} Collection | - | {self.item_title(item)}")
|
||||
self.library.alter_collection(item, self.name, smart_label_collection=self.smart_label_collection, add=False)
|
||||
logger.info(f"{self.name} {self.Type} | - | {util.item_title(item)}")
|
||||
if self.playlist:
|
||||
playlist_removes.append(item)
|
||||
else:
|
||||
self.library.alter_collection(item, self.name, smart_label_collection=self.smart_label_collection, add=False)
|
||||
amount_removed += 1
|
||||
if self.details["collection_changes_webhooks"]:
|
||||
if self.library.is_movie and item.ratingKey in self.library.movie_rating_key_map:
|
||||
remove_id = self.library.movie_rating_key_map[item.ratingKey]
|
||||
|
@ -1771,7 +1789,8 @@ class CollectionBuilder:
|
|||
else:
|
||||
remove_id = None
|
||||
self.notification_removals.append({"title": item.title, "id": remove_id})
|
||||
amount_removed += 1
|
||||
if self.playlist and playlist_removes:
|
||||
self.obj.removeItems(playlist_removes)
|
||||
if amount_removed > 0:
|
||||
logger.info("")
|
||||
logger.info(f"{amount_removed} {self.collection_level.capitalize()}{'s' if amount_removed == 1 else ''} Removed")
|
||||
|
@ -1914,10 +1933,10 @@ class CollectionBuilder:
|
|||
if self.check_tmdb_filter(missing_id, True, item=movie, check_released=self.details["missing_only_released"]):
|
||||
missing_movies_with_names.append((current_title, missing_id))
|
||||
if self.details["show_missing"] is True:
|
||||
logger.info(f"{self.name} Collection | ? | {current_title} (TMDb: {missing_id})")
|
||||
logger.info(f"{self.name} {self.Type} | ? | {current_title} (TMDb: {missing_id})")
|
||||
else:
|
||||
if self.details["show_filtered"] is True and self.details["show_missing"] is True:
|
||||
logger.info(f"{self.name} Collection | X | {current_title} (TMDb: {missing_id})")
|
||||
logger.info(f"{self.name} {self.Type} | X | {current_title} (TMDb: {missing_id})")
|
||||
logger.info("")
|
||||
logger.info(f"{len(missing_movies_with_names)} Movie{'s' if len(missing_movies_with_names) > 1 else ''} Missing")
|
||||
if len(missing_movies_with_names) > 0:
|
||||
|
@ -1949,10 +1968,10 @@ class CollectionBuilder:
|
|||
if self.check_tmdb_filter(missing_id, False, check_released=self.details["missing_only_released"]):
|
||||
missing_shows_with_names.append((show.title, missing_id))
|
||||
if self.details["show_missing"] is True:
|
||||
logger.info(f"{self.name} Collection | ? | {show.title} (TVDB: {missing_id})")
|
||||
logger.info(f"{self.name} {self.Type} | ? | {show.title} (TVDB: {missing_id})")
|
||||
else:
|
||||
if self.details["show_filtered"] is True and self.details["show_missing"] is True:
|
||||
logger.info(f"{self.name} Collection | X | {show.title} (TVDb: {missing_id})")
|
||||
logger.info(f"{self.name} {self.Type} | X | {show.title} (TVDb: {missing_id})")
|
||||
logger.info("")
|
||||
logger.info(f"{len(missing_shows_with_names)} Show{'s' if len(missing_shows_with_names) > 1 else ''} Missing")
|
||||
if len(missing_shows_with_names) > 0:
|
||||
|
@ -1975,46 +1994,23 @@ class CollectionBuilder:
|
|||
self.run_again_shows.extend(missing_tvdb_ids)
|
||||
if len(self.missing_parts) > 0 and self.library.is_show and self.details["save_missing"] is True:
|
||||
for missing in self.missing_parts:
|
||||
logger.info(f"{self.name} Collection | X | {missing}")
|
||||
logger.info(f"{self.name} {self.Type} | X | {missing}")
|
||||
return added_to_radarr, added_to_sonarr
|
||||
|
||||
def item_title(self, item):
|
||||
if self.collection_level == "season":
|
||||
if f"Season {item.index}" == item.title:
|
||||
return f"{item.parentTitle} {item.title}"
|
||||
else:
|
||||
return f"{item.parentTitle} Season {item.index}: {item.title}"
|
||||
elif self.collection_level == "episode":
|
||||
text = f"{item.grandparentTitle} S{util.add_zero(item.parentIndex)}E{util.add_zero(item.index)}"
|
||||
if f"Season {item.parentIndex}" == item.parentTitle:
|
||||
return f"{text}: {item.title}"
|
||||
else:
|
||||
return f"{text}: {item.parentTitle}: {item.title}"
|
||||
elif self.collection_level == "movie" and item.year:
|
||||
return f"{item.title} ({item.year})"
|
||||
else:
|
||||
return item.title
|
||||
|
||||
def load_collection_items(self):
|
||||
if self.build_collection and self.obj:
|
||||
self.items = self.library.get_collection_items(self.obj, self.smart_label_collection)
|
||||
elif not self.build_collection:
|
||||
logger.info("")
|
||||
util.separator(f"Items Found for {self.name} Collection", space=False, border=False)
|
||||
util.separator(f"Items Found for {self.name} {self.Type}", space=False, border=False)
|
||||
logger.info("")
|
||||
for rk in self.rating_keys:
|
||||
try:
|
||||
item = self.fetch_item(rk)
|
||||
logger.info(f"{item.title} (Rating Key: {rk})")
|
||||
self.items.append(item)
|
||||
except Failed as e:
|
||||
logger.error(e)
|
||||
self.items = self.added_items
|
||||
if not self.items:
|
||||
raise Failed(f"Plex Error: No Collection items found")
|
||||
raise Failed(f"Plex Error: No {self.Type} items found")
|
||||
|
||||
def update_item_details(self):
|
||||
logger.info("")
|
||||
util.separator(f"Updating Details of the Items in {self.name} Collection", space=False, border=False)
|
||||
util.separator(f"Updating Details of the Items in {self.name} {self.Type}", space=False, border=False)
|
||||
logger.info("")
|
||||
overlay = None
|
||||
overlay_folder = None
|
||||
|
@ -2136,13 +2132,13 @@ class CollectionBuilder:
|
|||
self.library.create_smart_collection(self.name, smart_type, self.smart_url)
|
||||
except Failed:
|
||||
raise Failed(f"{self.Type} Error: Label: {self.name} was not added to any items in the Library")
|
||||
self.obj = self.library.get_collection(self.name)
|
||||
self.obj = self.library.get_playlist(self.name) if self.playlist else self.library.get_collection(self.name)
|
||||
if not self.exists:
|
||||
self.created = True
|
||||
|
||||
def update_details(self):
|
||||
logger.info("")
|
||||
util.separator(f"Updating Details of {self.name} Collection", space=False, border=False)
|
||||
util.separator(f"Updating Details of {self.name} {self.Type}", space=False, border=False)
|
||||
logger.info("")
|
||||
if self.smart_url and self.smart_url != self.library.smart_filter(self.obj):
|
||||
self.library.update_smart_collection(self.obj, self.smart_url)
|
||||
|
@ -2150,7 +2146,7 @@ class CollectionBuilder:
|
|||
|
||||
edits = {}
|
||||
def get_summary(summary_method, summaries):
|
||||
logger.info(f"Detail: {summary_method} updated Collection Summary")
|
||||
logger.info(f"Detail: {summary_method} updated {self.Type} Summary")
|
||||
return summaries[summary_method]
|
||||
if "summary" in self.summaries: summary = get_summary("summary", self.summaries)
|
||||
elif "tmdb_description" in self.summaries: summary = get_summary("tmdb_description", self.summaries)
|
||||
|
@ -2176,8 +2172,12 @@ class CollectionBuilder:
|
|||
else: summary = None
|
||||
if summary:
|
||||
if str(summary) != str(self.obj.summary):
|
||||
edits["summary.value"] = summary
|
||||
edits["summary.locked"] = 1
|
||||
if self.playlist:
|
||||
self.obj.edit(summary=str(summary))
|
||||
logger.info("Details: have been updated")
|
||||
else:
|
||||
edits["summary.value"] = summary
|
||||
edits["summary.locked"] = 1
|
||||
|
||||
if "sort_title" in self.details:
|
||||
if str(self.details["sort_title"]) != str(self.obj.titleSort):
|
||||
|
@ -2267,7 +2267,7 @@ class CollectionBuilder:
|
|||
elif "tvdb_show_details" in self.posters: self.collection_poster = ImageData("tvdb_show_details", self.posters["tvdb_show_details"])
|
||||
elif "tmdb_show_details" in self.posters: self.collection_poster = ImageData("tmdb_show_details", self.posters["tmdb_show_details"])
|
||||
else:
|
||||
logger.info("No poster collection detail or asset folder found")
|
||||
logger.info(f"No poster {self.type} detail or asset folder found")
|
||||
|
||||
self.collection_background = None
|
||||
if len(self.backgrounds) > 0:
|
||||
|
@ -2286,35 +2286,28 @@ class CollectionBuilder:
|
|||
elif "tvdb_show_details" in self.backgrounds: self.collection_background = ImageData("tvdb_show_details", self.backgrounds["tvdb_show_details"], is_poster=False)
|
||||
elif "tmdb_show_details" in self.backgrounds: self.collection_background = ImageData("tmdb_show_details", self.backgrounds["tmdb_show_details"], is_poster=False)
|
||||
else:
|
||||
logger.info("No background collection detail or asset folder found")
|
||||
logger.info(f"No background {self.type} detail or asset folder found")
|
||||
|
||||
if self.collection_poster or self.collection_background:
|
||||
self.library.upload_images(self.obj, poster=self.collection_poster, background=self.collection_background)
|
||||
|
||||
def sort_collection(self):
|
||||
logger.info("")
|
||||
util.separator(f"Sorting {self.name} Collection", space=False, border=False)
|
||||
util.separator(f"Sorting {self.name} {self.Type}", space=False, border=False)
|
||||
logger.info("")
|
||||
if self.sort_by:
|
||||
search_data = self.build_filter("plex_search", {"sort_by": self.sort_by, "any": {"collection": self.name}})
|
||||
keys = {}
|
||||
rating_keys = []
|
||||
for item in self.library.get_filter_items(search_data[2]):
|
||||
keys[item.ratingKey] = item
|
||||
rating_keys.append(item.ratingKey)
|
||||
items = self.library.get_filter_items(search_data[2])
|
||||
else:
|
||||
keys = {_i.ratingKey: _i for _i in self.library.get_collection_items(self.obj, self.smart_label_collection)}
|
||||
rating_keys = self.rating_keys
|
||||
items = self.added_items
|
||||
previous = None
|
||||
logger.debug(keys)
|
||||
logger.debug(rating_keys)
|
||||
for key in rating_keys:
|
||||
text = f"after {self.item_title(keys[previous])}" if previous else "to the beginning"
|
||||
logger.info(f"Moving {self.item_title(keys[key])} {text}")
|
||||
self.library.move_item(self.obj, key, after=previous)
|
||||
previous = key
|
||||
for item in items:
|
||||
text = f"after {util.item_title(previous)}" if previous else "to the beginning"
|
||||
logger.info(f"Moving {util.item_title(item)} {text}")
|
||||
self.library.moveItem(self.obj, item, previous)
|
||||
previous = item
|
||||
|
||||
def send_notifications(self):
|
||||
def send_notifications(self, playlist=False):
|
||||
if self.obj and self.details["collection_changes_webhooks"] and \
|
||||
(self.created or len(self.notification_additions) > 0 or len(self.notification_removals) > 0):
|
||||
self.obj.reload()
|
||||
|
@ -2327,7 +2320,8 @@ class CollectionBuilder:
|
|||
created=self.created,
|
||||
deleted=self.deleted,
|
||||
additions=self.notification_additions,
|
||||
removals=self.notification_removals
|
||||
removals=self.notification_removals,
|
||||
playlist=playlist
|
||||
)
|
||||
except Failed as e:
|
||||
util.print_stacktrace()
|
||||
|
@ -2354,10 +2348,10 @@ class CollectionBuilder:
|
|||
logger.error(f"Plex Error: Item {rating_key} not found")
|
||||
continue
|
||||
if current in collection_items:
|
||||
logger.info(f"{name} Collection | = | {self.item_title(current)}")
|
||||
logger.info(f"{name} {self.Type} | = | {util.item_title(current)}")
|
||||
else:
|
||||
self.library.alter_collection(current, name, smart_label_collection=self.smart_label_collection)
|
||||
logger.info(f"{name} Collection | + | {self.item_title(current)}")
|
||||
logger.info(f"{name} {self.Type} | + | {util.item_title(current)}")
|
||||
if self.library.is_movie and current.ratingKey in self.library.movie_rating_key_map:
|
||||
add_id = self.library.movie_rating_key_map[current.ratingKey]
|
||||
elif self.library.is_show and current.ratingKey in self.library.show_rating_key_map:
|
||||
|
@ -2379,7 +2373,7 @@ class CollectionBuilder:
|
|||
continue
|
||||
if self.details["show_missing"] is True:
|
||||
current_title = f"{movie.title} ({util.validate_date(movie.release_date, 'test').year})" if movie.release_date else movie.title
|
||||
logger.info(f"{name} Collection | ? | {current_title} (TMDb: {missing_id})")
|
||||
logger.info(f"{name} {self.Type} | ? | {current_title} (TMDb: {missing_id})")
|
||||
logger.info("")
|
||||
logger.info(f"{len(self.run_again_movies)} Movie{'s' if len(self.run_again_movies) > 1 else ''} Missing")
|
||||
|
||||
|
@ -2393,5 +2387,5 @@ class CollectionBuilder:
|
|||
logger.error(e)
|
||||
continue
|
||||
if self.details["show_missing"] is True:
|
||||
logger.info(f"{name} Collection | ? | {title} (TVDb: {missing_id})")
|
||||
logger.info(f"{name} {self.Type} | ? | {title} (TVDb: {missing_id})")
|
||||
logger.info(f"{len(self.run_again_shows)} Show{'s' if len(self.run_again_shows) > 1 else ''} Missing")
|
||||
|
|
|
@ -13,6 +13,7 @@ from modules.letterboxd import Letterboxd
|
|||
from modules.mal import MyAnimeList
|
||||
from modules.notifiarr import Notifiarr
|
||||
from modules.omdb import OMDb
|
||||
from modules.playlist import PlaylistFile
|
||||
from modules.plex import Plex
|
||||
from modules.radarr import Radarr
|
||||
from modules.sonarr import Sonarr
|
||||
|
@ -28,7 +29,7 @@ from ruamel import yaml
|
|||
|
||||
logger = logging.getLogger("Plex Meta Manager")
|
||||
|
||||
sync_modes = {"append": "Only Add Items to the Collection", "sync": "Add & Remove Items from the Collection"}
|
||||
sync_modes = {"append": "Only Add Items to the Collection or Playlist", "sync": "Add & Remove Items from the Collection or Playlist"}
|
||||
mass_update_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb"}
|
||||
|
||||
class ConfigFile:
|
||||
|
@ -94,6 +95,7 @@ class ConfigFile:
|
|||
hooks("collection_removal")
|
||||
new_config["libraries"][library]["webhooks"]["collection_changes"] = changes if changes else None
|
||||
if "libraries" in new_config: new_config["libraries"] = new_config.pop("libraries")
|
||||
if "playlists" in new_config: new_config["playlists"] = new_config.pop("playlists")
|
||||
if "settings" in new_config: new_config["settings"] = new_config.pop("settings")
|
||||
if "webhooks" in new_config:
|
||||
temp = new_config.pop("webhooks")
|
||||
|
@ -342,8 +344,6 @@ class ConfigFile:
|
|||
else:
|
||||
logger.warning("mal attribute not found")
|
||||
|
||||
util.separator()
|
||||
|
||||
self.AniDB = None
|
||||
if "anidb" in self.data:
|
||||
util.separator()
|
||||
|
@ -360,6 +360,58 @@ class ConfigFile:
|
|||
if self.AniDB is None:
|
||||
self.AniDB = AniDB(self, None)
|
||||
|
||||
util.separator()
|
||||
|
||||
self.playlist_names = []
|
||||
self.playlist_files = []
|
||||
if "playlists" in self.data:
|
||||
logger.info("Reading in Playlist Files")
|
||||
if self.data["playlists"] is None:
|
||||
raise Failed("Config Error: playlists attribute is blank")
|
||||
playlists_pairs = []
|
||||
paths_to_check = self.data["playlists"] if isinstance(self.data["playlists"], list) else [self.data["playlists"]]
|
||||
for path in paths_to_check:
|
||||
if isinstance(path, dict):
|
||||
def check_dict(attr):
|
||||
if attr in path:
|
||||
if path[attr] is None:
|
||||
err = f"Config Error: playlists {attr} is blank"
|
||||
self.errors.append(err)
|
||||
logger.error(err)
|
||||
else:
|
||||
return path[attr]
|
||||
|
||||
url = check_dict("url")
|
||||
if url:
|
||||
playlists_pairs.append(("URL", url))
|
||||
git = check_dict("git")
|
||||
if git:
|
||||
playlists_pairs.append(("Git", git))
|
||||
file = check_dict("file")
|
||||
if file:
|
||||
playlists_pairs.append(("File", file))
|
||||
folder = check_dict("folder")
|
||||
if folder:
|
||||
if os.path.isdir(folder):
|
||||
yml_files = util.glob_filter(os.path.join(folder, "*.yml"))
|
||||
if yml_files:
|
||||
playlists_pairs.extend([("File", yml) for yml in yml_files])
|
||||
else:
|
||||
logger.error(f"Config Error: No YAML (.yml) files found in {folder}")
|
||||
else:
|
||||
logger.error(f"Config Error: Folder not found: {folder}")
|
||||
else:
|
||||
playlists_pairs.append(("File", path))
|
||||
for file_type, playlist_file in playlists_pairs:
|
||||
try:
|
||||
playlist_obj = PlaylistFile(self, file_type, playlist_file)
|
||||
self.playlist_names.extend([p for p in playlist_obj.playlists])
|
||||
self.playlist_files.append(playlist_obj)
|
||||
except Failed as e:
|
||||
util.print_multiline(e, error=True)
|
||||
else:
|
||||
logger.warning("playlists attribute not found")
|
||||
|
||||
self.TVDb = TVDb(self, self.general["tvdb_language"])
|
||||
self.IMDb = IMDb(self)
|
||||
self.Convert = Convert(self)
|
||||
|
@ -423,7 +475,6 @@ class ConfigFile:
|
|||
for library_name, lib in libs.items():
|
||||
if self.requested_libraries and library_name not in self.requested_libraries:
|
||||
continue
|
||||
util.separator()
|
||||
params = {
|
||||
"mapping_name": str(library_name),
|
||||
"name": str(lib["library_name"]) if lib and "library_name" in lib and lib["library_name"] else str(library_name)
|
||||
|
@ -674,10 +725,10 @@ class ConfigFile:
|
|||
self.notify(e)
|
||||
raise
|
||||
|
||||
def notify(self, text, library=None, collection=None, critical=True):
|
||||
def notify(self, text, server=None, library=None, collection=None, playlist=None, critical=True):
|
||||
for error in util.get_list(text, split=False):
|
||||
try:
|
||||
self.Webhooks.error_hooks(error, library=library, collection=collection, critical=critical)
|
||||
self.Webhooks.error_hooks(error, server=server, library=library, collection=collection, playlist=playlist, critical=critical)
|
||||
except Failed as e:
|
||||
util.print_stacktrace()
|
||||
logger.error(f"Webhooks Error: {e}")
|
||||
|
|
|
@ -81,7 +81,7 @@ class FlixPatrol:
|
|||
list_url = flixpatrol_list.strip()
|
||||
if not list_url.startswith(tuple([v for k, v in urls.items()])):
|
||||
fails = "\n".join([f"{v} (For {k.replace('_', ' ').title()})" for k, v in urls.items()])
|
||||
raise Failed(f"FlixPatrol Error: {list_url} must begin with either:{fails}")
|
||||
raise Failed(f"FlixPatrol Error: {list_url} must begin with either:\n{fails}")
|
||||
elif len(self._parse_list(list_url, language, is_movie)) > 0:
|
||||
valid_lists.append(list_url)
|
||||
else:
|
||||
|
|
|
@ -192,8 +192,9 @@ class Library(ABC):
|
|||
if background_uploaded:
|
||||
self.config.Cache.update_image_map(item.ratingKey, f"{self.image_table_name}_backgrounds", item.art, background.compare)
|
||||
|
||||
@abstractmethod
|
||||
def notify(self, text, collection=None, critical=True):
|
||||
self.config.notify(text, library=self, collection=collection, critical=critical)
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def _upload_image(self, item, image):
|
||||
|
|
66
modules/playlist.py
Normal file
66
modules/playlist.py
Normal file
|
@ -0,0 +1,66 @@
|
|||
import logging, os, re
|
||||
from datetime import datetime
|
||||
from modules import plex, util
|
||||
from modules.util import Failed, ImageData
|
||||
from plexapi.exceptions import NotFound
|
||||
from ruamel import yaml
|
||||
|
||||
logger = logging.getLogger("Plex Meta Manager")
|
||||
|
||||
github_base = "https://raw.githubusercontent.com/meisnate12/Plex-Meta-Manager-Configs/master/"
|
||||
|
||||
class PlaylistFile:
|
||||
def __init__(self, config, file_type, path):
|
||||
self.config = config
|
||||
self.type = file_type
|
||||
self.path = path
|
||||
self.playlists = {}
|
||||
self.templates = {}
|
||||
try:
|
||||
logger.info("")
|
||||
logger.info(f"Loading Playlist File {file_type}: {path}")
|
||||
if file_type in ["URL", "Git"]:
|
||||
content_path = path if file_type == "URL" else f"{github_base}{path}.yml"
|
||||
response = self.config.get(content_path)
|
||||
if response.status_code >= 400:
|
||||
raise Failed(f"URL Error: No file found at {content_path}")
|
||||
content = response.content
|
||||
elif os.path.exists(os.path.abspath(path)):
|
||||
content = open(path, encoding="utf-8")
|
||||
else:
|
||||
raise Failed(f"File Error: File does not exist {path}")
|
||||
data, ind, bsi = yaml.util.load_yaml_guess_indent(content)
|
||||
if data and "playlists" in data:
|
||||
if data["playlists"]:
|
||||
if isinstance(data["playlists"], dict):
|
||||
for _name, _data in data["playlists"].items():
|
||||
if _name in self.config.playlist_names:
|
||||
logger.error(f"Config Warning: Skipping duplicate playlist: {_name}")
|
||||
elif _data is None:
|
||||
logger.error(f"Config Warning: playlist: {_name} has no data")
|
||||
elif not isinstance(_data, dict):
|
||||
logger.error(f"Config Warning: playlist: {_name} must be a dictionary")
|
||||
else:
|
||||
self.playlists[str(_name)] = _data
|
||||
else:
|
||||
logger.warning(f"Config Warning: playlists must be a dictionary")
|
||||
else:
|
||||
logger.warning(f"Config Warning: playlists attribute is blank")
|
||||
if not self.playlists:
|
||||
raise Failed("YAML Error: playlists attribute is required")
|
||||
if data and "templates" in data:
|
||||
if data["templates"]:
|
||||
if isinstance(data["templates"], dict):
|
||||
for _name, _data in data["templates"].items():
|
||||
self.templates[str(_name)] = _data
|
||||
else:
|
||||
logger.warning(f"Config Warning: templates must be a dictionary")
|
||||
else:
|
||||
logger.warning(f"Config Warning: templates attribute is blank")
|
||||
|
||||
logger.info(f"Playlist File Loaded Successfully")
|
||||
except yaml.scanner.ScannerError as ye:
|
||||
raise Failed(f"YAML Error: {util.tab_new_lines(ye)}")
|
||||
except Exception as e:
|
||||
util.print_stacktrace()
|
||||
raise Failed(f"YAML Error: {e}")
|
|
@ -5,6 +5,7 @@ from modules.util import Failed, ImageData
|
|||
from plexapi import utils
|
||||
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
|
||||
from plexapi.collection import Collection
|
||||
from plexapi.playlist import Playlist
|
||||
from plexapi.server import PlexServer
|
||||
from retrying import retry
|
||||
from urllib import parse
|
||||
|
@ -264,6 +265,9 @@ class Plex(Library):
|
|||
self.tmdb_collections = None
|
||||
logger.error("Config Error: tmdb_collections only work with Movie Libraries.")
|
||||
|
||||
def notify(self, text, collection=None, critical=True):
|
||||
self.config.notify(text, server=self.PlexServer.friendlyName, library=self.name, collection=collection, critical=critical)
|
||||
|
||||
def set_server_preroll(self, preroll):
|
||||
self.PlexServer.settings.get('cinemaTrailersPrerollID').set(preroll)
|
||||
self.PlexServer.settings.save()
|
||||
|
@ -304,10 +308,18 @@ class Plex(Library):
|
|||
logger.info(util.adjust_space(f"Loaded {self.Plex._totalViewSize} {self.type}s"))
|
||||
return results
|
||||
|
||||
@retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex)
|
||||
def create_playlist(self, name, items):
|
||||
return self.PlexServer.createPlaylist(name, items=items)
|
||||
|
||||
@retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex)
|
||||
def fetchItems(self, key, container_start, container_size):
|
||||
return self.Plex.fetchItems(key, container_start=container_start, container_size=container_size)
|
||||
|
||||
@retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex)
|
||||
def moveItem(self, obj, item, after):
|
||||
obj.moveItem(item, after=after)
|
||||
|
||||
@retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex)
|
||||
def query(self, method):
|
||||
return method()
|
||||
|
@ -475,6 +487,12 @@ class Plex(Library):
|
|||
key += f"&promotedToSharedHome={1 if (shared is None and visibility['shared']) or shared else 0}"
|
||||
self._query(key, post=True)
|
||||
|
||||
def get_playlist(self, title):
|
||||
try:
|
||||
return self.PlexServer.playlist(title)
|
||||
except NotFound:
|
||||
raise Failed(f"Plex Error: Playlist {title} not found")
|
||||
|
||||
def get_collection(self, data):
|
||||
if isinstance(data, int):
|
||||
return self.fetchItem(data)
|
||||
|
@ -553,7 +571,7 @@ class Plex(Library):
|
|||
def get_collection_items(self, collection, smart_label_collection):
|
||||
if smart_label_collection:
|
||||
return self.get_labeled_items(collection.title if isinstance(collection, Collection) else str(collection))
|
||||
elif isinstance(collection, Collection):
|
||||
elif isinstance(collection, (Collection, Playlist)):
|
||||
if collection.smart:
|
||||
return self.get_filter_items(self.smart_filter(collection))
|
||||
else:
|
||||
|
@ -566,7 +584,7 @@ class Plex(Library):
|
|||
return self.Plex._search(key, None, 0, plexapi.X_PLEX_CONTAINER_SIZE)
|
||||
|
||||
def get_collection_name_and_items(self, collection, smart_label_collection):
|
||||
name = collection.title if isinstance(collection, Collection) else str(collection)
|
||||
name = collection.title if isinstance(collection, (Collection, Playlist)) else str(collection)
|
||||
return name, self.get_collection_items(collection, smart_label_collection)
|
||||
|
||||
def get_tmdb_from_map(self, item):
|
||||
|
|
|
@ -3,6 +3,7 @@ from datetime import datetime, timedelta
|
|||
from logging.handlers import RotatingFileHandler
|
||||
from pathvalidate import is_valid_filename, sanitize_filename
|
||||
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
|
||||
from plexapi.video import Season, Episode, Movie
|
||||
|
||||
try:
|
||||
import msvcrt
|
||||
|
@ -241,6 +242,23 @@ def validate_filename(filename):
|
|||
mapping_name = sanitize_filename(filename)
|
||||
return mapping_name, f"Log Folder Name: {filename} is invalid using {mapping_name}"
|
||||
|
||||
def item_title(item):
|
||||
if isinstance(item, Season):
|
||||
if f"Season {item.index}" == item.title:
|
||||
return f"{item.parentTitle} {item.title}"
|
||||
else:
|
||||
return f"{item.parentTitle} Season {item.index}: {item.title}"
|
||||
elif isinstance(item, Episode):
|
||||
text = f"{item.grandparentTitle} S{add_zero(item.parentIndex)}E{add_zero(item.index)}"
|
||||
if f"Season {item.parentIndex}" == item.parentTitle:
|
||||
return f"{text}: {item.title}"
|
||||
else:
|
||||
return f"{text}: {item.parentTitle}: {item.title}"
|
||||
elif isinstance(item, Movie) and item.year:
|
||||
return f"{item.title} ({item.year})"
|
||||
else:
|
||||
return item.title
|
||||
|
||||
def is_locked(filepath):
|
||||
locked = None
|
||||
file_object = None
|
||||
|
|
|
@ -60,17 +60,20 @@ class Webhooks:
|
|||
"added_to_sonarr": stats["sonarr"],
|
||||
})
|
||||
|
||||
def error_hooks(self, text, library=None, collection=None, critical=True):
|
||||
def error_hooks(self, text, server=None, library=None, collection=None, playlist=None, critical=True):
|
||||
if self.error_webhooks:
|
||||
json = {"error": str(text), "critical": critical}
|
||||
if server:
|
||||
json["server_name"] = str(server)
|
||||
if library:
|
||||
json["server_name"] = library.PlexServer.friendlyName
|
||||
json["library_name"] = library.name
|
||||
json["library_name"] = str(library)
|
||||
if collection:
|
||||
json["collection"] = str(collection)
|
||||
if playlist:
|
||||
json["playlist"] = str(playlist)
|
||||
self._request(self.error_webhooks, json)
|
||||
|
||||
def collection_hooks(self, webhooks, collection, poster_url=None, background_url=None, created=False, deleted=False, additions=None, removals=None):
|
||||
def collection_hooks(self, webhooks, collection, poster_url=None, background_url=None, created=False, deleted=False, additions=None, removals=None, playlist=False):
|
||||
if self.library:
|
||||
thumb = None
|
||||
if not poster_url and collection.thumb and next((f for f in collection.fields if f.name == "thumb"), None):
|
||||
|
@ -82,7 +85,7 @@ class Webhooks:
|
|||
"server_name": self.library.PlexServer.friendlyName,
|
||||
"library_name": self.library.name,
|
||||
"type": "movie" if self.library.is_movie else "show",
|
||||
"collection": collection.title,
|
||||
"playlist" if playlist else "collection": collection.title,
|
||||
"created": created,
|
||||
"deleted": deleted,
|
||||
"poster": thumb,
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import argparse, logging, os, sys, time
|
||||
from datetime import datetime
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
from plexapi.exceptions import NotFound
|
||||
from plexapi.video import Show, Season
|
||||
|
||||
try:
|
||||
import plexapi, schedule
|
||||
from modules import util
|
||||
|
@ -279,6 +283,290 @@ def update_libraries(config):
|
|||
util.print_stacktrace()
|
||||
util.print_multiline(e, critical=True)
|
||||
|
||||
|
||||
|
||||
if config.playlist_files:
|
||||
library_map = {_l.original_mapping_name: _l for _l in config.libraries}
|
||||
os.makedirs(os.path.join(default_dir, "logs", "playlists"), exist_ok=True)
|
||||
pf_file_logger = os.path.join(default_dir, "logs", "playlists", "playlists.log")
|
||||
should_roll_over = os.path.isfile(pf_file_logger)
|
||||
playlists_handler = RotatingFileHandler(pf_file_logger, delay=True, mode="w", backupCount=3, encoding="utf-8")
|
||||
util.apply_formatter(playlists_handler)
|
||||
if should_roll_over:
|
||||
playlists_handler.doRollover()
|
||||
logger.addHandler(playlists_handler)
|
||||
|
||||
logger.info("")
|
||||
util.separator("Playlists")
|
||||
logger.info("")
|
||||
for playlist_file in config.playlist_files:
|
||||
for mapping_name, playlist_attrs in playlist_file.playlists.items():
|
||||
playlist_start = datetime.now()
|
||||
if config.test_mode and ("test" not in playlist_attrs or playlist_attrs["test"] is not True):
|
||||
no_template_test = True
|
||||
if "template" in playlist_attrs and playlist_attrs["template"]:
|
||||
for data_template in util.get_list(playlist_attrs["template"], split=False):
|
||||
if "name" in data_template \
|
||||
and data_template["name"] \
|
||||
and playlist_file.templates \
|
||||
and data_template["name"] in playlist_file.templates \
|
||||
and playlist_file.templates[data_template["name"]] \
|
||||
and "test" in playlist_file.templates[data_template["name"]] \
|
||||
and playlist_file.templates[data_template["name"]]["test"] is True:
|
||||
no_template_test = False
|
||||
if no_template_test:
|
||||
continue
|
||||
|
||||
if "name_mapping" in playlist_attrs and playlist_attrs["name_mapping"]:
|
||||
playlist_log_name, output_str = util.validate_filename(playlist_attrs["name_mapping"])
|
||||
else:
|
||||
playlist_log_name, output_str = util.validate_filename(mapping_name)
|
||||
playlist_log_folder = os.path.join(default_dir, "logs", "playlists", playlist_log_name)
|
||||
os.makedirs(playlist_log_folder, exist_ok=True)
|
||||
ply_file_logger = os.path.join(playlist_log_folder, "playlist.log")
|
||||
should_roll_over = os.path.isfile(ply_file_logger)
|
||||
playlist_handler = RotatingFileHandler(ply_file_logger, delay=True, mode="w", backupCount=3, encoding="utf-8")
|
||||
util.apply_formatter(playlist_handler)
|
||||
if should_roll_over:
|
||||
playlist_handler.doRollover()
|
||||
logger.addHandler(playlist_handler)
|
||||
server_name = None
|
||||
library_names = None
|
||||
try:
|
||||
util.separator(f"{mapping_name} Playlist")
|
||||
logger.info("")
|
||||
if output_str:
|
||||
logger.info(output_str)
|
||||
logger.info("")
|
||||
if "libraries" not in playlist_attrs or not playlist_attrs["libraries"]:
|
||||
raise Failed("Playlist Error: libraries attribute is required and cannot be blank")
|
||||
pl_libraries = []
|
||||
for pl_library in util.get_list(playlist_attrs["libraries"]):
|
||||
if str(pl_library) in library_map:
|
||||
pl_libraries.append(library_map[pl_library])
|
||||
else:
|
||||
raise Failed(f"Playlist Error: Library: {pl_library} not defined")
|
||||
server_check = None
|
||||
for pl_library in pl_libraries:
|
||||
if server_check:
|
||||
if pl_library.PlexServer.machineIdentifier != server_check:
|
||||
raise Failed("Playlist Error: All defined libraries must be on the same server")
|
||||
else:
|
||||
server_check = pl_library.PlexServer.machineIdentifier
|
||||
|
||||
util.separator(f"Validating {mapping_name} Attributes", space=False, border=False)
|
||||
|
||||
builder = CollectionBuilder(config, pl_libraries[0], playlist_file, mapping_name, no_missing, playlist_attrs, playlist=True)
|
||||
logger.info("")
|
||||
|
||||
util.separator(f"Running {mapping_name} Playlist", space=False, border=False)
|
||||
|
||||
if len(builder.schedule) > 0:
|
||||
util.print_multiline(builder.schedule, info=True)
|
||||
|
||||
items_added = 0
|
||||
items_removed = 0
|
||||
logger.info("")
|
||||
logger.info(f"Sync Mode: {'sync' if builder.sync else 'append'}")
|
||||
|
||||
if builder.filters or builder.tmdb_filters:
|
||||
logger.info("")
|
||||
for filter_key, filter_value in builder.filters:
|
||||
logger.info(f"Playlist Filter {filter_key}: {filter_value}")
|
||||
for filter_key, filter_value in builder.tmdb_filters:
|
||||
logger.info(f"Playlist Filter {filter_key}: {filter_value}")
|
||||
|
||||
method, value = builder.builders[0]
|
||||
logger.debug("")
|
||||
logger.debug(f"Builder: {method}: {value}")
|
||||
logger.info("")
|
||||
items = []
|
||||
ids = builder.gather_ids(method, value)
|
||||
|
||||
if len(ids) > 0:
|
||||
total_ids = len(ids)
|
||||
logger.debug("")
|
||||
logger.debug(f"{total_ids} IDs Found: {ids}")
|
||||
for i, input_data in enumerate(ids, 1):
|
||||
input_id, id_type = input_data
|
||||
util.print_return(f"Parsing ID {i}/{total_ids}")
|
||||
if id_type == "tvdb_season":
|
||||
show_id, season_num = input_id.split("_")
|
||||
show_id = int(show_id)
|
||||
found = False
|
||||
for pl_library in pl_libraries:
|
||||
if show_id in pl_library.show_map:
|
||||
found = True
|
||||
show_item = pl_library.fetchItem(pl_library.show_map[show_id][0])
|
||||
try:
|
||||
items.extend(show_item.season(season=int(season_num)).episodes())
|
||||
except NotFound:
|
||||
builder.missing_parts.append(f"{show_item.title} Season: {season_num} Missing")
|
||||
break
|
||||
if not found and show_id not in builder.missing_shows:
|
||||
builder.missing_shows.append(show_id)
|
||||
elif id_type == "tvdb_episode":
|
||||
show_id, season_num, episode_num = input_id.split("_")
|
||||
show_id = int(show_id)
|
||||
found = False
|
||||
for pl_library in pl_libraries:
|
||||
if show_id in pl_library.show_map:
|
||||
found = True
|
||||
show_item = pl_library.fetchItem(pl_library.show_map[show_id][0])
|
||||
try:
|
||||
items.append(show_item.episode(season=int(season_num), episode=int(episode_num)))
|
||||
except NotFound:
|
||||
builder.missing_parts.append(f"{show_item.title} Season: {season_num} Episode: {episode_num} Missing")
|
||||
break
|
||||
if not found and show_id not in builder.missing_shows:
|
||||
builder.missing_shows.append(show_id)
|
||||
else:
|
||||
rating_keys = []
|
||||
if id_type == "ratingKey":
|
||||
rating_keys = input_id
|
||||
elif id_type == "tmdb":
|
||||
if input_id not in builder.ignore_ids:
|
||||
found = False
|
||||
for pl_library in pl_libraries:
|
||||
if input_id in pl_library.movie_map:
|
||||
found = True
|
||||
rating_keys = pl_library.movie_map[input_id]
|
||||
break
|
||||
if not found and input_id not in builder.missing_movies:
|
||||
builder.missing_movies.append(input_id)
|
||||
elif id_type in ["tvdb", "tmdb_show"]:
|
||||
if id_type == "tmdb_show":
|
||||
try:
|
||||
input_id = config.Convert.tmdb_to_tvdb(input_id, fail=True)
|
||||
except Failed as e:
|
||||
logger.error(e)
|
||||
continue
|
||||
if input_id not in builder.ignore_ids:
|
||||
found = False
|
||||
for pl_library in pl_libraries:
|
||||
if input_id in pl_library.show_map:
|
||||
found = True
|
||||
rating_keys = pl_library.show_map[input_id]
|
||||
break
|
||||
if not found and input_id not in builder.missing_shows:
|
||||
builder.missing_shows.append(input_id)
|
||||
elif id_type == "imdb":
|
||||
if input_id not in builder.ignore_imdb_ids:
|
||||
found = False
|
||||
for pl_library in pl_libraries:
|
||||
if input_id in pl_library.imdb_map:
|
||||
found = True
|
||||
rating_keys = pl_library.imdb_map[input_id]
|
||||
break
|
||||
if not found and builder.do_missing:
|
||||
try:
|
||||
tmdb_id, tmdb_type = config.Convert.imdb_to_tmdb(input_id, fail=True)
|
||||
if tmdb_type == "movie":
|
||||
if tmdb_id not in builder.missing_movies:
|
||||
builder.missing_movies.append(tmdb_id)
|
||||
else:
|
||||
tvdb_id = config.Convert.tmdb_to_tvdb(tmdb_id, fail=True)
|
||||
if tvdb_id not in builder.missing_shows:
|
||||
builder.missing_shows.append(tvdb_id)
|
||||
except Failed as e:
|
||||
logger.error(e)
|
||||
continue
|
||||
if not isinstance(rating_keys, list):
|
||||
rating_keys = [rating_keys]
|
||||
for rk in rating_keys:
|
||||
try:
|
||||
item = builder.fetch_item(rk)
|
||||
if isinstance(item, (Show, Season)):
|
||||
items.extend(item.episodes())
|
||||
else:
|
||||
items.append(item)
|
||||
except Failed as e:
|
||||
logger.error(e)
|
||||
util.print_end()
|
||||
|
||||
if len(items) > 0:
|
||||
builder.filter_and_save_items(items)
|
||||
|
||||
if len(builder.added_items) >= builder.minimum:
|
||||
logger.info("")
|
||||
util.separator(f"Adding to {mapping_name} Playlist", space=False, border=False)
|
||||
logger.info("")
|
||||
items_added = builder.add_to_collection()
|
||||
stats["added"] += items_added
|
||||
items_removed = 0
|
||||
if builder.sync:
|
||||
items_removed = builder.sync_collection()
|
||||
stats["removed"] += items_removed
|
||||
elif len(builder.added_items) < builder.minimum:
|
||||
logger.info("")
|
||||
logger.info(f"Playlist Minimum: {builder.minimum} not met for {mapping_name} Playlist")
|
||||
if builder.details["delete_below_minimum"] and builder.obj:
|
||||
builder.delete_collection()
|
||||
builder.deleted = True
|
||||
logger.info("")
|
||||
logger.info(f"Playlist {builder.obj.title} deleted")
|
||||
|
||||
if builder.do_missing and (len(builder.missing_movies) > 0 or len(builder.missing_shows) > 0):
|
||||
if builder.details["show_missing"] is True:
|
||||
logger.info("")
|
||||
util.separator(f"Missing from Library", space=False, border=False)
|
||||
logger.info("")
|
||||
radarr_add, sonarr_add = builder.run_missing()
|
||||
stats["radarr"] += radarr_add
|
||||
stats["sonarr"] += sonarr_add
|
||||
|
||||
run_item_details = True
|
||||
try:
|
||||
builder.load_collection()
|
||||
if builder.created:
|
||||
stats["created"] += 1
|
||||
elif items_added > 0 or items_removed > 0:
|
||||
stats["modified"] += 1
|
||||
except Failed:
|
||||
util.print_stacktrace()
|
||||
run_item_details = False
|
||||
logger.info("")
|
||||
util.separator("No Playlist to Update", space=False, border=False)
|
||||
else:
|
||||
builder.update_details()
|
||||
|
||||
if builder.deleted:
|
||||
stats["deleted"] += 1
|
||||
|
||||
if (builder.item_details or builder.custom_sort) and run_item_details and builder.builders:
|
||||
try:
|
||||
builder.load_collection_items()
|
||||
except Failed:
|
||||
logger.info("")
|
||||
util.separator("No Items Found", space=False, border=False)
|
||||
else:
|
||||
if builder.item_details:
|
||||
builder.update_item_details()
|
||||
if builder.custom_sort:
|
||||
builder.sort_collection()
|
||||
|
||||
builder.send_notifications()
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
except NotScheduled as e:
|
||||
util.print_multiline(e, info=True)
|
||||
except Failed as e:
|
||||
config.notify(e, server=server_name, library=library_names, playlist=mapping_name)
|
||||
util.print_stacktrace()
|
||||
util.print_multiline(e, error=True)
|
||||
except Exception as e:
|
||||
config.notify(f"Unknown Error: {e}", server=server_name, library=library_names, playlist=mapping_name)
|
||||
util.print_stacktrace()
|
||||
logger.error(f"Unknown Error: {e}")
|
||||
logger.info("")
|
||||
util.separator(f"Finished {mapping_name} Playlist\nPlaylist Run Time: {str(datetime.now() - playlist_start).split('.')[0]}")
|
||||
logger.removeHandler(playlist_handler)
|
||||
logger.removeHandler(playlists_handler)
|
||||
|
||||
has_run_again = False
|
||||
for library in config.libraries:
|
||||
if library.run_again:
|
||||
|
@ -675,7 +963,7 @@ def run_collection(config, library, metadata, requested_collections):
|
|||
|
||||
builder.find_rating_keys()
|
||||
|
||||
if len(builder.rating_keys) >= builder.minimum and builder.build_collection:
|
||||
if len(builder.added_items) >= builder.minimum and builder.build_collection:
|
||||
logger.info("")
|
||||
util.separator(f"Adding to {mapping_name} Collection", space=False, border=False)
|
||||
logger.info("")
|
||||
|
@ -685,7 +973,7 @@ def run_collection(config, library, metadata, requested_collections):
|
|||
if builder.sync:
|
||||
items_removed = builder.sync_collection()
|
||||
stats["removed"] += items_removed
|
||||
elif len(builder.rating_keys) < builder.minimum and builder.build_collection:
|
||||
elif len(builder.added_items) < builder.minimum and builder.build_collection:
|
||||
logger.info("")
|
||||
logger.info(f"Collection Minimum: {builder.minimum} not met for {mapping_name} Collection")
|
||||
if builder.details["delete_below_minimum"] and builder.obj:
|
||||
|
@ -718,9 +1006,6 @@ def run_collection(config, library, metadata, requested_collections):
|
|||
util.separator("No Collection to Update", space=False, border=False)
|
||||
else:
|
||||
builder.update_details()
|
||||
if builder.custom_sort or builder.sort_by:
|
||||
library.run_sort.append(builder)
|
||||
# builder.sort_collection()
|
||||
|
||||
if builder.deleted:
|
||||
stats["deleted"] += 1
|
||||
|
@ -730,14 +1015,18 @@ def run_collection(config, library, metadata, requested_collections):
|
|||
logger.info("")
|
||||
logger.info(f"Plex Server Movie pre-roll video updated to {builder.server_preroll}")
|
||||
|
||||
if builder.item_details and run_item_details and builder.builders:
|
||||
if (builder.item_details or builder.custom_sort or builder.sort_by) and run_item_details and builder.builders:
|
||||
try:
|
||||
builder.load_collection_items()
|
||||
except Failed:
|
||||
logger.info("")
|
||||
util.separator("No Items Found", space=False, border=False)
|
||||
else:
|
||||
builder.update_item_details()
|
||||
if builder.item_details:
|
||||
builder.update_item_details()
|
||||
if builder.custom_sort or builder.sort_by:
|
||||
library.run_sort.append(builder)
|
||||
# builder.sort_collection()
|
||||
|
||||
builder.send_notifications()
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
PlexAPI==4.8.0
|
||||
tmdbv3api==1.7.6
|
||||
arrapi==1.2.8
|
||||
lxml==4.6.4
|
||||
lxml==4.7.1
|
||||
requests==2.26.0
|
||||
ruamel.yaml==0.17.17
|
||||
schedule==1.1.0
|
||||
|
|
Loading…
Reference in a new issue