$484 add playlists

pull/509/head
meisnate12 3 years ago
parent 03b02a2575
commit a5a27d25da

@ -85,7 +85,7 @@ boolean_details = [
string_details = ["sort_title", "content_rating", "name_mapping"] string_details = ["sort_title", "content_rating", "name_mapping"]
ignored_details = [ ignored_details = [
"smart_filter", "smart_label", "smart_url", "run_again", "schedule", "sync_mode", "template", "test", "delete_not_scheduled", "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", details = ["ignore_ids", "ignore_imdb_ids", "server_preroll", "collection_changes_webhooks", "collection_mode",
"collection_minimum", "label"] + boolean_details + string_details "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_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" "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: class CollectionBuilder:
def __init__(self, config, library, metadata, name, no_missing, data, playlist=False): def __init__(self, config, library, metadata, name, no_missing, data, playlist=False):
@ -197,7 +202,7 @@ class CollectionBuilder:
self.builders = [] self.builders = []
self.filters = [] self.filters = []
self.tmdb_filters = [] self.tmdb_filters = []
self.rating_keys = [] self.added_items = []
self.filtered_keys = {} self.filtered_keys = {}
self.run_again_movies = [] self.run_again_movies = []
self.run_again_shows = [] self.run_again_shows = []
@ -460,7 +465,7 @@ class CollectionBuilder:
suffix = f" and could not be found to delete" suffix = f" and could not be found to delete"
raise NotScheduled(f"{self.schedule}\n\nCollection {self.name} not scheduled to run{suffix}") 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 self.validate_builders = True
if "validate_builders" in methods: 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.run_again = self._parse("run_again", self.data, datatype="bool", methods=methods, default=False)
self.build_collection = True self.build_collection = True
if "build_collection" in methods: if "build_collection" in methods and not self.playlist:
logger.debug("") logger.debug("")
logger.debug("Validating Method: build_collection") logger.debug("Validating Method: build_collection")
logger.debug(f"Value: {data[methods['build_collection']]}") logger.debug(f"Value: {data[methods['build_collection']]}")
@ -496,8 +501,8 @@ class CollectionBuilder:
else: else:
self.sync = self.data[methods["sync_mode"]].lower() == "sync" self.sync = self.data[methods["sync_mode"]].lower() == "sync"
self.custom_sort = False self.custom_sort = self.playlist
if "collection_order" in methods: if "collection_order" in methods and not self.playlist:
logger.debug("") logger.debug("")
logger.debug("Validating Method: collection_order") logger.debug("Validating Method: collection_order")
if self.data[methods["collection_order"]] is None: 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)") 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 self.sort_by = None
if "sort_by" in methods: if "sort_by" in methods and not self.playlist:
logger.debug("") logger.debug("")
logger.debug("Validating Method: sort_by") logger.debug("Validating Method: sort_by")
if self.data[methods["sort_by"]] is None: if self.data[methods["sort_by"]] is None:
@ -525,7 +530,9 @@ class CollectionBuilder:
self.sort_by = self.data[methods["sort_by"]] self.sort_by = self.data[methods["sort_by"]]
self.collection_level = "movie" if self.library.is_movie else "show" 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("")
logger.debug("Validating Method: collection_level") logger.debug("Validating Method: collection_level")
if self.library.is_movie: if self.library.is_movie:
@ -562,7 +569,7 @@ class CollectionBuilder:
self.smart_sort = "random" self.smart_sort = "random"
self.smart_label_collection = False self.smart_label_collection = False
if "smart_label" in methods: if "smart_label" in methods and not self.playlist:
logger.debug("") logger.debug("")
logger.debug("Validating Method: smart_label") logger.debug("Validating Method: smart_label")
self.smart_label_collection = True self.smart_label_collection = True
@ -579,7 +586,7 @@ class CollectionBuilder:
self.smart_url = None self.smart_url = None
self.smart_type_key = None self.smart_type_key = None
self.smart_filter_details = "" self.smart_filter_details = ""
if "smart_url" in methods: if "smart_url" in methods and not self.playlist:
logger.debug("") logger.debug("")
logger.debug("Validating Method: smart_url") logger.debug("Validating Method: smart_url")
if not self.data[methods["smart_url"]]: if not self.data[methods["smart_url"]]:
@ -591,7 +598,7 @@ class CollectionBuilder:
except ValueError: except ValueError:
raise Failed(f"{self.Type} Error: smart_url is incorrectly formatted") 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) 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): def cant_interact(attr1, attr2, fail=False):
@ -626,6 +633,7 @@ class CollectionBuilder:
try: 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") 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 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.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.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") 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: if self.build_collection:
try: 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): if (self.smart and not self.obj.smart) or (not self.smart and self.obj.smart):
logger.info("") logger.info("")
logger.error(f"{self.Type} Error: Converting {self.obj.title} to a {'smart' if self.smart else 'normal'} collection") 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)): if not isinstance(item, (Movie, Show, Season, Episode)):
logger.error(f"{self.Type} Error: Item: {item} must be Movie, Show, Season, or Episode") logger.error(f"{self.Type} Error: Item: {item} must be Movie, Show, Season, or Episode")
continue continue
if item.ratingKey not in self.rating_keys: if item not in self.added_items:
if item.ratingKey in self.filtered_keys: if item.ratingKey in self.filtered_keys:
if self.details["show_filtered"] is True: if self.details["show_filtered"] is True:
logger.info(f"{name} {self.Type} | X | {self.filtered_keys[item.ratingKey]}") logger.info(f"{name} {self.Type} | X | {self.filtered_keys[item.ratingKey]}")
else: 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}"): 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: else:
self.filtered_keys[item.ratingKey] = current_title self.filtered_keys[item.ratingKey] = current_title
if self.details["show_filtered"] is True: if self.details["show_filtered"] is True:
@ -1724,29 +1732,34 @@ class CollectionBuilder:
def add_to_collection(self): 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) 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 amount_added = 0
for i, item in enumerate(self.rating_keys, 1): playlist_adds = []
try: for item in self.added_items:
current = self.fetch_item(item) current_operation = "=" if item in collection_items else "+"
except Failed as e: logger.info(util.adjust_space(f"{name} {self.Type} | {current_operation} | {util.item_title(item)}"))
logger.error(e) if item in collection_items:
continue self.plex_map[item.ratingKey] = None
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
else: 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 amount_added += 1
if self.details["collection_changes_webhooks"]: if self.details["collection_changes_webhooks"]:
if self.library.is_movie and current.ratingKey in self.library.movie_rating_key_map: if self.library.is_movie and item.ratingKey in self.library.movie_rating_key_map:
add_id = self.library.movie_rating_key_map[current.ratingKey] add_id = self.library.movie_rating_key_map[item.ratingKey]
elif self.library.is_show and current.ratingKey in self.library.show_rating_key_map: elif self.library.is_show and item.ratingKey in self.library.show_rating_key_map:
add_id = self.library.show_rating_key_map[current.ratingKey] add_id = self.library.show_rating_key_map[item.ratingKey]
else: else:
add_id = None 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() util.print_end()
logger.info("") logger.info("")
logger.info(f"{total} {self.collection_level.capitalize()}{'s' if total > 1 else ''} Processed") logger.info(f"{total} {self.collection_level.capitalize()}{'s' if total > 1 else ''} Processed")
@ -1754,15 +1767,20 @@ class CollectionBuilder:
def sync_collection(self): def sync_collection(self):
amount_removed = 0 amount_removed = 0
playlist_removes = []
for ratingKey, item in self.plex_map.items(): for ratingKey, item in self.plex_map.items():
if item is not None: if item is not None:
if amount_removed == 0: if amount_removed == 0:
logger.info("") 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("") logger.info("")
self.library.reload(item) self.library.reload(item)
logger.info(f"{self.name} Collection | - | {self.item_title(item)}") logger.info(f"{self.name} {self.Type} | - | {util.item_title(item)}")
self.library.alter_collection(item, self.name, smart_label_collection=self.smart_label_collection, add=False) 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.details["collection_changes_webhooks"]:
if self.library.is_movie and item.ratingKey in self.library.movie_rating_key_map: 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] remove_id = self.library.movie_rating_key_map[item.ratingKey]
@ -1771,7 +1789,8 @@ class CollectionBuilder:
else: else:
remove_id = None remove_id = None
self.notification_removals.append({"title": item.title, "id": remove_id}) 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: if amount_removed > 0:
logger.info("") logger.info("")
logger.info(f"{amount_removed} {self.collection_level.capitalize()}{'s' if amount_removed == 1 else ''} Removed") 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"]): 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)) missing_movies_with_names.append((current_title, missing_id))
if self.details["show_missing"] is True: 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: else:
if self.details["show_filtered"] is True and self.details["show_missing"] is True: 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("")
logger.info(f"{len(missing_movies_with_names)} Movie{'s' if len(missing_movies_with_names) > 1 else ''} Missing") 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: 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"]): if self.check_tmdb_filter(missing_id, False, check_released=self.details["missing_only_released"]):
missing_shows_with_names.append((show.title, missing_id)) missing_shows_with_names.append((show.title, missing_id))
if self.details["show_missing"] is True: 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: else:
if self.details["show_filtered"] is True and self.details["show_missing"] is True: 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("")
logger.info(f"{len(missing_shows_with_names)} Show{'s' if len(missing_shows_with_names) > 1 else ''} Missing") 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: if len(missing_shows_with_names) > 0:
@ -1975,46 +1994,23 @@ class CollectionBuilder:
self.run_again_shows.extend(missing_tvdb_ids) 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: if len(self.missing_parts) > 0 and self.library.is_show and self.details["save_missing"] is True:
for missing in self.missing_parts: 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 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): def load_collection_items(self):
if self.build_collection and self.obj: if self.build_collection and self.obj:
self.items = self.library.get_collection_items(self.obj, self.smart_label_collection) self.items = self.library.get_collection_items(self.obj, self.smart_label_collection)
elif not self.build_collection: elif not self.build_collection:
logger.info("") 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("") logger.info("")
for rk in self.rating_keys: self.items = self.added_items
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)
if not self.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): def update_item_details(self):
logger.info("") 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("") logger.info("")
overlay = None overlay = None
overlay_folder = None overlay_folder = None
@ -2136,13 +2132,13 @@ class CollectionBuilder:
self.library.create_smart_collection(self.name, smart_type, self.smart_url) self.library.create_smart_collection(self.name, smart_type, self.smart_url)
except Failed: except Failed:
raise Failed(f"{self.Type} Error: Label: {self.name} was not added to any items in the Library") 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: if not self.exists:
self.created = True self.created = True
def update_details(self): def update_details(self):
logger.info("") 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("") logger.info("")
if self.smart_url and self.smart_url != self.library.smart_filter(self.obj): if self.smart_url and self.smart_url != self.library.smart_filter(self.obj):
self.library.update_smart_collection(self.obj, self.smart_url) self.library.update_smart_collection(self.obj, self.smart_url)
@ -2150,7 +2146,7 @@ class CollectionBuilder:
edits = {} edits = {}
def get_summary(summary_method, summaries): 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] return summaries[summary_method]
if "summary" in self.summaries: summary = get_summary("summary", self.summaries) if "summary" in self.summaries: summary = get_summary("summary", self.summaries)
elif "tmdb_description" in self.summaries: summary = get_summary("tmdb_description", self.summaries) elif "tmdb_description" in self.summaries: summary = get_summary("tmdb_description", self.summaries)
@ -2176,8 +2172,12 @@ class CollectionBuilder:
else: summary = None else: summary = None
if summary: if summary:
if str(summary) != str(self.obj.summary): if str(summary) != str(self.obj.summary):
edits["summary.value"] = summary if self.playlist:
edits["summary.locked"] = 1 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 "sort_title" in self.details:
if str(self.details["sort_title"]) != str(self.obj.titleSort): 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 "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"]) elif "tmdb_show_details" in self.posters: self.collection_poster = ImageData("tmdb_show_details", self.posters["tmdb_show_details"])
else: 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 self.collection_background = None
if len(self.backgrounds) > 0: 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 "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) elif "tmdb_show_details" in self.backgrounds: self.collection_background = ImageData("tmdb_show_details", self.backgrounds["tmdb_show_details"], is_poster=False)
else: 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: if self.collection_poster or self.collection_background:
self.library.upload_images(self.obj, poster=self.collection_poster, background=self.collection_background) self.library.upload_images(self.obj, poster=self.collection_poster, background=self.collection_background)
def sort_collection(self): def sort_collection(self):
logger.info("") 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("") logger.info("")
if self.sort_by: if self.sort_by:
search_data = self.build_filter("plex_search", {"sort_by": self.sort_by, "any": {"collection": self.name}}) search_data = self.build_filter("plex_search", {"sort_by": self.sort_by, "any": {"collection": self.name}})
keys = {} items = self.library.get_filter_items(search_data[2])
rating_keys = []
for item in self.library.get_filter_items(search_data[2]):
keys[item.ratingKey] = item
rating_keys.append(item.ratingKey)
else: else:
keys = {_i.ratingKey: _i for _i in self.library.get_collection_items(self.obj, self.smart_label_collection)} items = self.added_items
rating_keys = self.rating_keys
previous = None previous = None
logger.debug(keys) for item in items:
logger.debug(rating_keys) text = f"after {util.item_title(previous)}" if previous else "to the beginning"
for key in rating_keys: logger.info(f"Moving {util.item_title(item)} {text}")
text = f"after {self.item_title(keys[previous])}" if previous else "to the beginning" self.library.moveItem(self.obj, item, previous)
logger.info(f"Moving {self.item_title(keys[key])} {text}") previous = item
self.library.move_item(self.obj, key, after=previous)
previous = key def send_notifications(self, playlist=False):
def send_notifications(self):
if self.obj and self.details["collection_changes_webhooks"] and \ 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.created or len(self.notification_additions) > 0 or len(self.notification_removals) > 0):
self.obj.reload() self.obj.reload()
@ -2327,7 +2320,8 @@ class CollectionBuilder:
created=self.created, created=self.created,
deleted=self.deleted, deleted=self.deleted,
additions=self.notification_additions, additions=self.notification_additions,
removals=self.notification_removals removals=self.notification_removals,
playlist=playlist
) )
except Failed as e: except Failed as e:
util.print_stacktrace() util.print_stacktrace()
@ -2354,10 +2348,10 @@ class CollectionBuilder:
logger.error(f"Plex Error: Item {rating_key} not found") logger.error(f"Plex Error: Item {rating_key} not found")
continue continue
if current in collection_items: 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: else:
self.library.alter_collection(current, name, smart_label_collection=self.smart_label_collection) 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: 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] 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: elif self.library.is_show and current.ratingKey in self.library.show_rating_key_map:
@ -2379,7 +2373,7 @@ class CollectionBuilder:
continue continue
if self.details["show_missing"] is True: 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 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("")
logger.info(f"{len(self.run_again_movies)} Movie{'s' if len(self.run_again_movies) > 1 else ''} Missing") 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) logger.error(e)
continue continue
if self.details["show_missing"] is True: 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") 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.mal import MyAnimeList
from modules.notifiarr import Notifiarr from modules.notifiarr import Notifiarr
from modules.omdb import OMDb from modules.omdb import OMDb
from modules.playlist import PlaylistFile
from modules.plex import Plex from modules.plex import Plex
from modules.radarr import Radarr from modules.radarr import Radarr
from modules.sonarr import Sonarr from modules.sonarr import Sonarr
@ -28,7 +29,7 @@ from ruamel import yaml
logger = logging.getLogger("Plex Meta Manager") 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"} mass_update_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb"}
class ConfigFile: class ConfigFile:
@ -94,6 +95,7 @@ class ConfigFile:
hooks("collection_removal") hooks("collection_removal")
new_config["libraries"][library]["webhooks"]["collection_changes"] = changes if changes else None 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 "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 "settings" in new_config: new_config["settings"] = new_config.pop("settings")
if "webhooks" in new_config: if "webhooks" in new_config:
temp = new_config.pop("webhooks") temp = new_config.pop("webhooks")
@ -342,8 +344,6 @@ class ConfigFile:
else: else:
logger.warning("mal attribute not found") logger.warning("mal attribute not found")
util.separator()
self.AniDB = None self.AniDB = None
if "anidb" in self.data: if "anidb" in self.data:
util.separator() util.separator()
@ -360,6 +360,58 @@ class ConfigFile:
if self.AniDB is None: if self.AniDB is None:
self.AniDB = AniDB(self, 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.TVDb = TVDb(self, self.general["tvdb_language"])
self.IMDb = IMDb(self) self.IMDb = IMDb(self)
self.Convert = Convert(self) self.Convert = Convert(self)
@ -423,7 +475,6 @@ class ConfigFile:
for library_name, lib in libs.items(): for library_name, lib in libs.items():
if self.requested_libraries and library_name not in self.requested_libraries: if self.requested_libraries and library_name not in self.requested_libraries:
continue continue
util.separator()
params = { params = {
"mapping_name": str(library_name), "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) "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) self.notify(e)
raise 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): for error in util.get_list(text, split=False):
try: 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: except Failed as e:
util.print_stacktrace() util.print_stacktrace()
logger.error(f"Webhooks Error: {e}") logger.error(f"Webhooks Error: {e}")

@ -81,7 +81,7 @@ class FlixPatrol:
list_url = flixpatrol_list.strip() list_url = flixpatrol_list.strip()
if not list_url.startswith(tuple([v for k, v in urls.items()])): 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()]) 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: elif len(self._parse_list(list_url, language, is_movie)) > 0:
valid_lists.append(list_url) valid_lists.append(list_url)
else: else:

@ -192,8 +192,9 @@ class Library(ABC):
if background_uploaded: if background_uploaded:
self.config.Cache.update_image_map(item.ratingKey, f"{self.image_table_name}_backgrounds", item.art, background.compare) 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): def notify(self, text, collection=None, critical=True):
self.config.notify(text, library=self, collection=collection, critical=critical) pass
@abstractmethod @abstractmethod
def _upload_image(self, item, image): def _upload_image(self, item, image):

@ -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 import utils
from plexapi.exceptions import BadRequest, NotFound, Unauthorized from plexapi.exceptions import BadRequest, NotFound, Unauthorized
from plexapi.collection import Collection from plexapi.collection import Collection
from plexapi.playlist import Playlist
from plexapi.server import PlexServer from plexapi.server import PlexServer
from retrying import retry from retrying import retry
from urllib import parse from urllib import parse
@ -264,6 +265,9 @@ class Plex(Library):
self.tmdb_collections = None self.tmdb_collections = None
logger.error("Config Error: tmdb_collections only work with Movie Libraries.") 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): def set_server_preroll(self, preroll):
self.PlexServer.settings.get('cinemaTrailersPrerollID').set(preroll) self.PlexServer.settings.get('cinemaTrailersPrerollID').set(preroll)
self.PlexServer.settings.save() self.PlexServer.settings.save()
@ -304,10 +308,18 @@ class Plex(Library):
logger.info(util.adjust_space(f"Loaded {self.Plex._totalViewSize} {self.type}s")) logger.info(util.adjust_space(f"Loaded {self.Plex._totalViewSize} {self.type}s"))
return results 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) @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): def fetchItems(self, key, container_start, container_size):
return self.Plex.fetchItems(key, container_start=container_start, container_size=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) @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex)
def query(self, method): def query(self, method):
return 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}" key += f"&promotedToSharedHome={1 if (shared is None and visibility['shared']) or shared else 0}"
self._query(key, post=True) 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): def get_collection(self, data):
if isinstance(data, int): if isinstance(data, int):
return self.fetchItem(data) return self.fetchItem(data)
@ -553,7 +571,7 @@ class Plex(Library):
def get_collection_items(self, collection, smart_label_collection): def get_collection_items(self, collection, smart_label_collection):
if smart_label_collection: if smart_label_collection:
return self.get_labeled_items(collection.title if isinstance(collection, Collection) else str(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: if collection.smart:
return self.get_filter_items(self.smart_filter(collection)) return self.get_filter_items(self.smart_filter(collection))
else: else:
@ -566,7 +584,7 @@ class Plex(Library):
return self.Plex._search(key, None, 0, plexapi.X_PLEX_CONTAINER_SIZE) return self.Plex._search(key, None, 0, plexapi.X_PLEX_CONTAINER_SIZE)
def get_collection_name_and_items(self, collection, smart_label_collection): 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) return name, self.get_collection_items(collection, smart_label_collection)
def get_tmdb_from_map(self, item): def get_tmdb_from_map(self, item):

@ -3,6 +3,7 @@ from datetime import datetime, timedelta
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
from pathvalidate import is_valid_filename, sanitize_filename from pathvalidate import is_valid_filename, sanitize_filename
from plexapi.exceptions import BadRequest, NotFound, Unauthorized from plexapi.exceptions import BadRequest, NotFound, Unauthorized
from plexapi.video import Season, Episode, Movie
try: try:
import msvcrt import msvcrt
@ -241,6 +242,23 @@ def validate_filename(filename):
mapping_name = sanitize_filename(filename) mapping_name = sanitize_filename(filename)
return mapping_name, f"Log Folder Name: {filename} is invalid using {mapping_name}" 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): def is_locked(filepath):
locked = None locked = None
file_object = None file_object = None

@ -60,17 +60,20 @@ class Webhooks:
"added_to_sonarr": stats["sonarr"], "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: if self.error_webhooks:
json = {"error": str(text), "critical": critical} json = {"error": str(text), "critical": critical}
if server:
json["server_name"] = str(server)
if library: if library:
json["server_name"] = library.PlexServer.friendlyName json["library_name"] = str(library)
json["library_name"] = library.name
if collection: if collection:
json["collection"] = str(collection) json["collection"] = str(collection)
if playlist:
json["playlist"] = str(playlist)
self._request(self.error_webhooks, json) 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: if self.library:
thumb = None thumb = None
if not poster_url and collection.thumb and next((f for f in collection.fields if f.name == "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, "server_name": self.library.PlexServer.friendlyName,
"library_name": self.library.name, "library_name": self.library.name,
"type": "movie" if self.library.is_movie else "show", "type": "movie" if self.library.is_movie else "show",
"collection": collection.title, "playlist" if playlist else "collection": collection.title,
"created": created, "created": created,
"deleted": deleted, "deleted": deleted,
"poster": thumb, "poster": thumb,

@ -1,6 +1,10 @@
import argparse, logging, os, sys, time import argparse, logging, os, sys, time
from datetime import datetime from datetime import datetime
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
from plexapi.exceptions import NotFound
from plexapi.video import Show, Season
try: try:
import plexapi, schedule import plexapi, schedule
from modules import util from modules import util
@ -279,6 +283,290 @@ def update_libraries(config):
util.print_stacktrace() util.print_stacktrace()
util.print_multiline(e, critical=True) 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 has_run_again = False
for library in config.libraries: for library in config.libraries:
if library.run_again: if library.run_again:
@ -675,7 +963,7 @@ def run_collection(config, library, metadata, requested_collections):
builder.find_rating_keys() 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("") logger.info("")
util.separator(f"Adding to {mapping_name} Collection", space=False, border=False) util.separator(f"Adding to {mapping_name} Collection", space=False, border=False)
logger.info("") logger.info("")
@ -685,7 +973,7 @@ def run_collection(config, library, metadata, requested_collections):
if builder.sync: if builder.sync:
items_removed = builder.sync_collection() items_removed = builder.sync_collection()
stats["removed"] += items_removed 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("")
logger.info(f"Collection Minimum: {builder.minimum} not met for {mapping_name} Collection") logger.info(f"Collection Minimum: {builder.minimum} not met for {mapping_name} Collection")
if builder.details["delete_below_minimum"] and builder.obj: 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) util.separator("No Collection to Update", space=False, border=False)
else: else:
builder.update_details() builder.update_details()
if builder.custom_sort or builder.sort_by:
library.run_sort.append(builder)
# builder.sort_collection()
if builder.deleted: if builder.deleted:
stats["deleted"] += 1 stats["deleted"] += 1
@ -730,14 +1015,18 @@ def run_collection(config, library, metadata, requested_collections):
logger.info("") logger.info("")
logger.info(f"Plex Server Movie pre-roll video updated to {builder.server_preroll}") 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: try:
builder.load_collection_items() builder.load_collection_items()
except Failed: except Failed:
logger.info("") logger.info("")
util.separator("No Items Found", space=False, border=False) util.separator("No Items Found", space=False, border=False)
else: 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() builder.send_notifications()

@ -1,7 +1,7 @@
PlexAPI==4.8.0 PlexAPI==4.8.0
tmdbv3api==1.7.6 tmdbv3api==1.7.6
arrapi==1.2.8 arrapi==1.2.8
lxml==4.6.4 lxml==4.7.1
requests==2.26.0 requests==2.26.0
ruamel.yaml==0.17.17 ruamel.yaml==0.17.17
schedule==1.1.0 schedule==1.1.0

Loading…
Cancel
Save