#189 adds music library support and better filters

pull/556/head
meisnate12 3 years ago
parent 6788fa8ae4
commit e4d1851f47

@ -3,13 +3,13 @@ from datetime import datetime, timedelta
from modules import anidb, anilist, flixpatrol, icheckmovies, imdb, letterboxd, mal, plex, radarr, sonarr, stevenlu, tautulli, tmdb, trakt, tvdb, util
from modules.util import Failed, ImageData, NotScheduled
from PIL import Image
from plexapi.audio import Artist, Album, Track
from plexapi.exceptions import BadRequest, NotFound
from plexapi.video import Movie, Show, Season, Episode
from urllib.parse import quote
logger = logging.getLogger("Plex Meta Manager")
string_filters = ["title", "episode_title", "studio"]
advance_new_agent = ["item_metadata_language", "item_use_original_title"]
advance_show = ["item_episode_sorting", "item_keep_episodes", "item_delete_episodes", "item_season_display", "item_episode_sorting"]
method_alias = {
@ -46,6 +46,7 @@ method_alias = {
"collection_changes_webhooks": "changes_webhooks"
}
filter_translation = {
"record_label": "studio",
"actor": "actors",
"audience_rating": "audienceRating",
"collection": "collections",
@ -73,6 +74,7 @@ movie_only_builders = [
"tmdb_collection", "tmdb_collection_details", "tmdb_movie", "tmdb_movie_details", "tmdb_now_playing",
"tvdb_movie", "tvdb_movie_details", "trakt_boxoffice"
]
music_only_builders = ["item_album_sorting"]
summary_details = [
"summary", "tmdb_summary", "tmdb_description", "tmdb_biography", "tvdb_summary",
"tvdb_description", "trakt_description", "letterboxd_description", "icheckmovies_description"
@ -91,7 +93,7 @@ ignored_details = [
"validate_builders", "sort_by", "libraries", "sync_to_users", "collection_name", "playlist_name", "name"
]
details = ["ignore_ids", "ignore_imdb_ids", "server_preroll", "changes_webhooks", "collection_mode",
"collection_minimum", "label"] + boolean_details + scheduled_boolean + string_details
"collection_minimum", "label", "album_sorting"] + boolean_details + scheduled_boolean + string_details
collectionless_details = ["collection_order", "plex_collectionless", "label", "label_sync_mode", "test"] + \
poster_details + background_details + summary_details + string_details
item_bool_details = ["item_tmdb_season_titles", "item_assets", "revert_overlay", "item_lock_background", "item_lock_poster", "item_lock_title", "item_refresh"]
@ -102,56 +104,51 @@ sonarr_details = [
"sonarr_add", "sonarr_add_existing", "sonarr_folder", "sonarr_monitor", "sonarr_language", "sonarr_series",
"sonarr_quality", "sonarr_season", "sonarr_search", "sonarr_cutoff_search", "sonarr_tag"
]
parts_collection_valid = [
"plex_search", "trakt_list", "trakt_list_details", "collection_mode", "label", "visible_library", "changes_webhooks"
"visible_home", "visible_shared", "show_missing", "save_missing", "missing_only_released", "server_preroll",
"item_lock_background", "item_lock_poster", "item_lock_title", "item_refresh", "imdb_list"
] + summary_details + poster_details + background_details + string_details
all_filters = [
"actor", "actor.not",
"audio_language", "audio_language.not",
"audio_track_title", "audio_track_title.not", "audio_track_title.is", "audio_track_title.isnot", "audio_track_title.begins", "audio_track_title.ends", "audio_track_title.regex",
"collection", "collection.not",
"content_rating", "content_rating.not",
"country", "country.not",
"director", "director.not",
"filepath", "filepath.not", "filepath.is", "filepath.isnot", "filepath.begins", "filepath.ends", "filepath.regex",
"genre", "genre.not",
"label", "label.not",
"producer", "producer.not",
"release", "release.not", "release.before", "release.after", "release.regex", "history",
"added", "added.not", "added.before", "added.after", "added.regex",
"last_played", "last_played.not", "last_played.before", "last_played.after", "last_played.regex",
"first_episode_aired", "first_episode_aired.not", "first_episode_aired.before", "first_episode_aired.after", "first_episode_aired.regex",
"last_episode_aired", "last_episode_aired.not", "last_episode_aired.before", "last_episode_aired.after", "last_episode_aired.regex",
"title", "title.not", "title.is", "title.isnot", "title.begins", "title.ends", "title.regex",
"plays.gt", "plays.gte", "plays.lt", "plays.lte",
"tmdb_vote_count.gt", "tmdb_vote_count.gte", "tmdb_vote_count.lt", "tmdb_vote_count.lte",
"duration.gt", "duration.gte", "duration.lt", "duration.lte",
"original_language", "original_language.not",
"user_rating.gt", "user_rating.gte", "user_rating.lt", "user_rating.lte",
"audience_rating.gt", "audience_rating.gte", "audience_rating.lt", "audience_rating.lte",
"critic_rating.gt", "critic_rating.gte", "critic_rating.lt", "critic_rating.lte",
"studio", "studio.not", "studio.is", "studio.isnot", "studio.begins", "studio.ends", "studio.regex",
"subtitle_language", "subtitle_language.not",
"resolution", "resolution.not",
"writer", "writer.not", "has_collection", "has_overlay",
"year", "year.gt", "year.gte", "year.lt", "year.lte", "year.not"
"tmdb_year", "tmdb_year.gt", "tmdb_year.gte", "tmdb_year.lt", "tmdb_year.lte", "tmdb_year.not"
]
album_details = ["item_label", "item_album_sorting"]
filters_by_type = {
"movie_show_season_episode_artist_album_track": ["title", "collection", "has_collection", "added", "last_played", "user_rating", "plays"],
"movie_show_season_episode_album_track": ["year"],
"movie_show_episode_artist_track": ["filepath"],
"movie_show_episode_album": ["release", "critic_rating", "history"],
"movie_show_episode_track": ["duration"],
"movie_show_artist_album": ["genre"],
"movie_show_episode": ["actor", "content_rating", "audience_rating"],
"movie_show_album": ["label"],
"movie_episode_track": ["audio_track_title"],
"movie_show": ["studio", "original_language", "has_overlay", "tmdb_vote_count", "tmdb_year"],
"movie_episode": ["director", "producer", "writer", "resolution", "audio_language", "subtitle_language"],
"movie_artist": ["country"],
"show": ["network", "first_episode_aired", "last_episode_aired"],
"album": ["record_label"]
}
filters = {
"movie": [item for check, sub in filters_by_type.items() for item in sub if "movie" in check],
"show": [item for check, sub in filters_by_type.items() for item in sub if "show" in check],
"season": [item for check, sub in filters_by_type.items() for item in sub if "season" in check],
"episode": [item for check, sub in filters_by_type.items() for item in sub if "episode" in check],
"artist": [item for check, sub in filters_by_type.items() for item in sub if "artist" in check],
"album": [item for check, sub in filters_by_type.items() for item in sub if "album" in check],
"track": [item for check, sub in filters_by_type.items() for item in sub if "track" in check]
}
tmdb_filters = ["original_language", "tmdb_vote_count", "tmdb_year", "first_episode_aired", "last_episode_aired"]
boolean_filters = ["has_collection", "has_overlay"]
movie_only_filters = [
"audio_language", "audio_language.not",
"audio_track_title", "audio_track_title.not", "audio_track_title.is", "audio_track_title.isnot", "audio_track_title.begins", "audio_track_title.ends", "audio_track_title.regex",
"country", "country.not",
"director", "director.not",
"duration.gt", "duration.gte", "duration.lt", "duration.lte",
"subtitle_language", "subtitle_language.not",
"resolution", "resolution.not",
"writer", "writer.not"
string_filters = ["title", "studio", "record_label", "filepath", "audio_track_title"]
string_modifiers = ["", ".not", ".is", ".isnot", ".begins", ".ends", ".regex"]
tag_filters = [
"actor", "collection", "content_rating", "country", "director", "network", "genre", "label", "producer", "year",
"writer", "original_language", "resolution", "audio_language", "subtitle_language"
]
show_only_filters = ["first_episode_aired", "last_episode_aired", "network"]
tag_modifiers = ["", ".not"]
boolean_filters = ["has_collection", "has_overlay"]
date_filters = ["release", "added", "last_played", "first_episode_aired", "last_episode_aired"]
date_modifiers = ["", ".not", ".before", ".after", ".regex"]
number_filters = ["year", "tmdb_year", "critic_rating", "audience_rating", "user_rating", "tmdb_vote_count", "plays", "duration"]
number_modifiers = [".gt", ".gte", ".lt", ".lte"]
special_filters = ["history"]
all_filters = boolean_filters + special_filters + \
[f"{f}{m}" for f in string_filters for m in string_modifiers] + \
[f"{f}{m}" for f in tag_filters for m in tag_modifiers] + \
[f"{f}{m}" for f in date_filters for m in date_modifiers] + \
[f"{f}{m}" for f in number_filters for m in number_modifiers]
smart_invalid = ["collection_order", "collection_level"]
smart_url_invalid = ["filters", "run_again", "sync_mode", "show_filtered", "show_missing", "save_missing", "smart_label"] + radarr_details + sonarr_details
custom_sort_builders = [
@ -168,11 +165,20 @@ 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"
]
parts_collection_valid = [
"plex_search", "trakt_list", "trakt_list_details", "collection_mode", "label", "visible_library", "changes_webhooks"
"visible_home", "visible_shared", "show_missing", "save_missing", "missing_only_released", "server_preroll",
"item_lock_background", "item_lock_poster", "item_lock_title", "item_refresh", "imdb_list"
] + summary_details + poster_details + background_details + string_details
playlist_attributes = [
"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", "changes_webhooks", "collection_minimum",
] + custom_sort_builders + summary_details + poster_details + radarr_details + sonarr_details
music_attributes = [
"item_label", "item_assets", "item_lock_background", "item_lock_poster", "item_lock_title",
"item_refresh", "plex_search", "plex_all", "filters"
] + details + summary_details + poster_details + background_details
class CollectionBuilder:
def __init__(self, config, library, metadata, name, no_missing, data, playlist=False, valid_users=None):
@ -347,16 +353,22 @@ class CollectionBuilder:
logger.debug("")
logger.debug("Validating Method: collection_level")
if self.library.is_movie:
raise Failed(f"{self.Type} Error: collection_level attribute only works for show libraries")
raise Failed(f"{self.Type} Error: collection_level attribute only works for show and music libraries")
elif self.data[methods["collection_level"]] is None:
raise Failed(f"{self.Type} Error: collection_level attribute is blank")
else:
logger.debug(f"Value: {self.data[methods['collection_level']]}")
if self.data[methods["collection_level"]].lower() in plex.collection_level_options:
if (self.library.is_show and self.data[methods["collection_level"]].lower() in plex.collection_level_show_options) or \
(self.library.is_music and self.data[methods["collection_level"]].lower() in plex.collection_level_music_options):
self.collection_level = self.data[methods["collection_level"]].lower()
else:
raise Failed(f"{self.Type} Error: {self.data[methods['collection_level']]} collection_level invalid\n\tseason (Collection at the Season Level)\n\tepisode (Collection at the Episode Level)")
self.parts_collection = self.collection_level in ["season", "episode"]
if self.library.is_show:
options = "\n\tseason (Collection at the Season Level)\n\tepisode (Collection at the Episode Level)"
else:
options = "\n\talbum (Collection at the Album Level)\n\ttrack (Collection at the Track Level)"
raise Failed(f"{self.Type} Error: {self.data[methods['collection_level']]} collection_level invalid{options}")
self.parts_collection = self.collection_level in plex.collection_level_options
if "tmdb_person" in methods:
logger.debug("")
@ -380,7 +392,7 @@ class CollectionBuilder:
self.smart_sort = "random"
self.smart_label_collection = False
if "smart_label" in methods and not self.playlist:
if "smart_label" in methods and not self.playlist and not self.library.is_music:
logger.debug("")
logger.debug("Validating Method: smart_label")
self.smart_label_collection = True
@ -441,44 +453,88 @@ class CollectionBuilder:
logger.debug(f"Validating Method: {method_key}")
logger.debug(f"Value: {method_data}")
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")
elif not self.library.Tautulli and "tautulli" in method_name: raise Failed(f"{self.Type} Error: {method_final} requires Tautulli to be configured")
elif not self.config.MyAnimeList and "mal" in method_name: raise Failed(f"{self.Type} Error: {method_final} requires MyAnimeList to be configured")
elif self.library.is_movie and method_name in show_only_builders: raise Failed(f"{self.Type} Error: {method_final} attribute only works for show libraries")
elif self.library.is_show and method_name in movie_only_builders: raise Failed(f"{self.Type} Error: {method_final} attribute only works for movie libraries")
elif self.library.is_show and method_name in plex.movie_only_searches: raise Failed(f"{self.Type} Error: {method_final} plex search only works for movie libraries")
elif self.library.is_movie and method_name in plex.show_only_searches: raise Failed(f"{self.Type} Error: {method_final} plex search only works for show libraries")
elif self.parts_collection and method_name not in parts_collection_valid: raise Failed(f"{self.Type} Error: {method_final} attribute does not work with Collection Level: {self.collection_level.capitalize()}")
elif self.smart and method_name in smart_invalid: raise Failed(f"{self.Type} Error: {method_final} attribute only works with normal collections")
elif self.collectionless and method_name not in collectionless_details: raise Failed(f"{self.Type} Error: {method_final} attribute does not work for Collectionless collection")
elif self.smart_url and method_name in all_builders + smart_url_invalid: raise Failed(f"{self.Type} Error: {method_final} builder not allowed when using smart_filter")
elif method_name in summary_details: self._summary(method_name, method_data)
elif method_name in poster_details: self._poster(method_name, method_data)
elif method_name in background_details: self._background(method_name, method_data)
elif method_name in details: self._details(method_name, method_data, method_final, methods)
elif method_name in item_details: self._item_details(method_name, method_data, method_mod, method_final, methods)
elif method_name in radarr_details: self._radarr(method_name, method_data)
elif method_name in sonarr_details: self._sonarr(method_name, method_data)
elif method_name in anidb.builders: self._anidb(method_name, method_data)
elif method_name in anilist.builders: self._anilist(method_name, method_data)
elif method_name in flixpatrol.builders: self._flixpatrol(method_name, method_data)
elif method_name in icheckmovies.builders: self._icheckmovies(method_name, method_data)
elif method_name in letterboxd.builders: self._letterboxd(method_name, method_data)
elif method_name in imdb.builders: self._imdb(method_name, method_data)
elif method_name in mal.builders: self._mal(method_name, method_data)
elif method_name in plex.builders or method_final in plex.searches: self._plex(method_name, method_data)
elif method_name in stevenlu.builders: self._stevenlu(method_name, method_data)
elif method_name in tautulli.builders: self._tautulli(method_name, method_data)
elif method_name in tmdb.builders: self._tmdb(method_name, method_data)
elif method_name in trakt.builders: self._trakt(method_name, method_data)
elif method_name in tvdb.builders: self._tvdb(method_name, method_data)
elif method_name == "filters": self._filters(method_name, method_data)
else: raise Failed(f"{self.Type} Error: {method_final} attribute not supported")
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")
elif not self.library.Tautulli and "tautulli" in method_name:
raise Failed(f"{self.Type} Error: {method_final} requires Tautulli to be configured")
elif not self.config.MyAnimeList and "mal" in method_name:
raise Failed(f"{self.Type} Error: {method_final} requires MyAnimeList to be configured")
elif self.library.is_movie and method_name in show_only_builders:
raise Failed(f"{self.Type} Error: {method_final} attribute only allowed for show libraries")
elif self.library.is_show and method_name in movie_only_builders:
raise Failed(f"{self.Type} Error: {method_final} attribute only allowed for movie libraries")
elif self.library.is_show and method_name in plex.movie_only_searches:
raise Failed(f"{self.Type} Error: {method_final} plex search only allowed for movie libraries")
elif self.library.is_movie and method_name in plex.show_only_searches:
raise Failed(f"{self.Type} Error: {method_final} plex search only allowed for show libraries")
elif self.library.is_music and method_name not in music_attributes:
raise Failed(f"{self.Type} Error: {method_final} attribute not allowed for music libraries")
elif self.library.is_music and method_name in album_details and self.collection_level != "album":
raise Failed(f"{self.Type} Error: {method_final} attribute only allowed for album collections")
elif not self.library.is_music and method_name in music_only_builders:
raise Failed(f"{self.Type} Error: {method_final} attribute only allowed for music libraries")
elif self.parts_collection and method_name not in parts_collection_valid:
raise Failed(f"{self.Type} Error: {method_final} attribute not allowed with Collection Level: {self.collection_level.capitalize()}")
elif self.smart and method_name in smart_invalid:
raise Failed(f"{self.Type} Error: {method_final} attribute only allowed with normal collections")
elif self.collectionless and method_name not in collectionless_details:
raise Failed(f"{self.Type} Error: {method_final} attribute not allowed for Collectionless collection")
elif self.smart_url and method_name in all_builders + smart_url_invalid:
raise Failed(f"{self.Type} Error: {method_final} builder not allowed when using smart_filter")
elif method_name in summary_details:
self._summary(method_name, method_data)
elif method_name in poster_details:
self._poster(method_name, method_data)
elif method_name in background_details:
self._background(method_name, method_data)
elif method_name in details:
self._details(method_name, method_data, method_final, methods)
elif method_name in item_details:
self._item_details(method_name, method_data, method_mod, method_final, methods)
elif method_name in radarr_details:
self._radarr(method_name, method_data)
elif method_name in sonarr_details:
self._sonarr(method_name, method_data)
elif method_name in anidb.builders:
self._anidb(method_name, method_data)
elif method_name in anilist.builders:
self._anilist(method_name, method_data)
elif method_name in flixpatrol.builders:
self._flixpatrol(method_name, method_data)
elif method_name in icheckmovies.builders:
self._icheckmovies(method_name, method_data)
elif method_name in letterboxd.builders:
self._letterboxd(method_name, method_data)
elif method_name in imdb.builders:
self._imdb(method_name, method_data)
elif method_name in mal.builders:
self._mal(method_name, method_data)
elif method_name in plex.builders or method_final in plex.searches:
self._plex(method_name, method_data)
elif method_name in stevenlu.builders:
self._stevenlu(method_name, method_data)
elif method_name in tautulli.builders:
self._tautulli(method_name, method_data)
elif method_name in tmdb.builders:
self._tmdb(method_name, method_data)
elif method_name in trakt.builders:
self._trakt(method_name, method_data)
elif method_name in tvdb.builders:
self._tvdb(method_name, method_data)
elif method_name == "filters":
self._filters(method_name, method_data)
else:
raise Failed(f"{self.Type} Error: {method_final} attribute not supported")
except Failed as e:
if self.validate_builders:
raise
@ -502,7 +558,7 @@ class CollectionBuilder:
if "add_existing" not in self.sonarr_details:
self.sonarr_details["add_existing"] = self.library.Sonarr.add_existing if self.library.Sonarr else False
if self.smart_url or self.collectionless:
if self.smart_url or self.collectionless or self.library.is_music:
self.radarr_details["add"] = False
self.radarr_details["add_existing"] = False
self.sonarr_details["add"] = False
@ -1123,10 +1179,8 @@ class CollectionBuilder:
message = None
if filter_final not in all_filters:
message = f"{self.Type} Error: {filter_final} is not a valid filter attribute"
elif filter_final in movie_only_filters and self.library.is_show:
message = f"{self.Type} Error: {filter_final} filter attribute only works for movie libraries"
elif filter_final in show_only_filters and self.library.is_movie:
message = f"{self.Type} Error: {filter_final} filter attribute only works for show libraries"
elif self.collection_level in filters and filter_attr not in filters[self.collection_level]:
message = f"{self.Type} Error: {filter_final} is not a valid {self.collection_level} filter attribute"
elif filter_final is None:
message = f"{self.Type} Error: {filter_final} filter attribute is blank"
elif filter_attr in tmdb_filters:
@ -1280,8 +1334,8 @@ class CollectionBuilder:
logger.info("")
logger.info("Filtering Builders:")
for i, item in enumerate(items, 1):
if not isinstance(item, (Movie, Show, Season, Episode)):
logger.error(f"{self.Type} Error: Item: {item} must be Movie, Show, Season, or Episode")
if not isinstance(item, (Movie, Show, Season, Episode, Artist, Album, Track)):
logger.error(f"{self.Type} Error: Item: {item} is an invalid type")
continue
if item not in self.added_items:
if item.ratingKey in self.filtered_keys:
@ -1318,8 +1372,14 @@ class CollectionBuilder:
if plex_filter[filter_alias["type"]] not in ["shows", "seasons", "episodes"]:
raise Failed(f"{self.Type} Error: type: {plex_filter[filter_alias['type']]} is invalid, must be either shows, season, or episodes")
sort_type = plex_filter[filter_alias["type"]]
elif smart and "type" in filter_alias and self.library.is_music:
if plex_filter[filter_alias["type"]] not in ["artists", "albums", "tracks"]:
raise Failed(f"{self.Type} Error: type: {plex_filter[filter_alias['type']]} is invalid, must be either artists, albums, or tracks")
sort_type = plex_filter[filter_alias["type"]]
elif self.library.is_show:
sort_type = "shows"
elif self.library.is_music:
sort_type = "artists"
else:
sort_type = "movies"
ms = method.split("_")
@ -1369,7 +1429,7 @@ class CollectionBuilder:
mod = plex.modifier_translation[modifier] if modifier in plex.modifier_translation else modifier
if arg_s is None:
arg_s = arg
if attr in string_filters and modifier in ["", ".not"]:
if attr in plex.string_attributes and modifier in ["", ".not"]:
mod_s = "does not contain" if modifier == ".not" else "contains"
elif mod_s is None:
mod_s = util.mod_displays[modifier]
@ -1379,10 +1439,14 @@ class CollectionBuilder:
if final_attr not in plex.searches and not final_attr.startswith(("any", "all")):
raise Failed(f"{self.Type} Error: {final_attr} is not a valid {method} attribute")
elif final_attr in plex.movie_only_searches and self.library.is_show:
elif self.library.is_show and final_attr in plex.movie_only_searches:
raise Failed(f"{self.Type} Error: {final_attr} {method} attribute only works for movie libraries")
elif final_attr in plex.show_only_searches and self.library.is_movie:
elif self.library.is_movie and final_attr in plex.show_only_searches:
raise Failed(f"{self.Type} Error: {final_attr} {method} attribute only works for show libraries")
elif self.library.is_music and final_attr not in plex.music_searches:
raise Failed(f"{self.Type} Error: {final_attr} {method} attribute only works for movie or show libraries")
elif not self.library.is_music and final_attr in plex.music_searches:
raise Failed(f"{self.Type} Error: {final_attr} {method} attribute only works for music libraries")
elif _data is None:
raise Failed(f"{self.Type} Error: {final_attr} {method} attribute is blank")
elif final_attr.startswith(("any", "all")):
@ -1410,11 +1474,11 @@ class CollectionBuilder:
bool_mod = "" if validation else "!"
bool_arg = "true" if validation else "false"
results, display_add = build_url_arg(1, mod=bool_mod, arg_s=bool_arg, mod_s="is")
elif (attr in ["title", "episode_title", "studio", "decade", "year", "episode_year"] or attr in plex.tags) and modifier in ["", ".is", ".isnot", ".not", ".begins", ".ends"]:
elif (attr in plex.tag_attributes + plex.string_attributes + plex.year_attributes) and modifier in ["", ".is", ".isnot", ".not", ".begins", ".ends"]:
results = ""
display_add = ""
for og_value, result in validation:
built_arg = build_url_arg(quote(str(result)) if attr in string_filters else result, arg_s=og_value)
built_arg = build_url_arg(quote(str(result)) if attr in plex.string_attributes else result, arg_s=og_value)
display_add += built_arg[1]
results += f"{conjunction if len(results) > 0 else ''}{built_arg[0]}"
else:
@ -1475,7 +1539,7 @@ class CollectionBuilder:
else:
logger.error(err)
return valid_regex
elif attribute in ["title", "studio", "episode_title", "audio_track_title"] and modifier in ["", ".not", ".is", ".isnot", ".begins", ".ends"]:
elif attribute in plex.string_attributes + ["audio_track_title"] and modifier in ["", ".not", ".is", ".isnot", ".begins", ".ends"]:
return smart_pair(util.get_list(data, split=False))
elif attribute == "original_language":
return util.get_list(data, lower=True)
@ -1488,7 +1552,7 @@ class CollectionBuilder:
if str(data).lower() in ["day", "month"]:
return data.lower()
raise Failed(f"{self.Type} Error: history attribute invalid: {data} must be a number between 1-30, day, or month")
elif attribute in plex.tags and modifier in ["", ".not"]:
elif attribute in plex.tag_attributes and modifier in ["", ".not"]:
if attribute in plex.tmdb_attributes:
final_values = []
for value in util.get_list(data):
@ -1517,20 +1581,18 @@ class CollectionBuilder:
else:
logger.error(error)
return valid_list
elif attribute in ["year", "episode_year", "tmdb_year"] and modifier in [".gt", ".gte", ".lt", ".lte"]:
return self._parse(final, data, datatype="int", minimum=1800, maximum=self.current_year)
elif attribute in plex.date_attributes and modifier in [".before", ".after"]:
return util.validate_date(data, final, return_as="%Y-%m-%d")
elif attribute in plex.number_attributes and modifier in ["", ".not", ".gt", ".gte", ".lt", ".lte"]:
return self._parse(final, data, datatype="int")
elif attribute in plex.float_attributes and modifier in [".gt", ".gte", ".lt", ".lte"]:
return self._parse(final, data, datatype="float", minimum=0, maximum=10)
elif attribute in ["decade", "year", "episode_year", "tmdb_year"] and modifier in ["", ".not"]:
elif attribute in plex.year_attributes + ["tmdb_year"] and modifier in ["", ".not"]:
final_years = []
values = util.get_list(data)
for value in values:
final_years.append(self._parse(final, value, datatype="int", minimum=1800, maximum=self.current_year))
final_years.append(self._parse(final, value, datatype="int"))
return smart_pair(final_years)
elif attribute in plex.number_attributes + plex.date_attributes and modifier in ["", ".not", ".gt", ".gte", ".lt", ".lte"]:
return self._parse(final, data, datatype="int")
elif attribute in plex.float_attributes and modifier in [".gt", ".gte", ".lt", ".lte"]:
return self._parse(final, data, datatype="float", minimum=0, maximum=10)
elif attribute in plex.boolean_attributes + boolean_filters:
return self._parse(attribute, data, datatype="bool")
else:
@ -1556,8 +1618,8 @@ class CollectionBuilder:
def fetch_item(self, item):
try:
current = self.library.fetchItem(item.ratingKey if isinstance(item, (Movie, Show, Season, Episode)) else int(item))
if not isinstance(current, (Movie, Show, Season, Episode)):
current = self.library.fetchItem(item.ratingKey if isinstance(item, (Movie, Show, Season, Episode, Artist, Album, Track)) else int(item))
if not isinstance(current, (Movie, Show, Season, Episode, Artist, Album, Track)):
raise NotFound
return current
except (BadRequest, NotFound):
@ -1668,53 +1730,73 @@ class CollectionBuilder:
return False
return True
def check_filters(self, current, display):
def check_filters(self, item, display):
if (self.filters or self.tmdb_filters) and not self.details["only_filter_missing"]:
util.print_return(f"Filtering {display} {current.title}")
if self.tmdb_filters:
if current.ratingKey not in self.library.movie_rating_key_map and current.ratingKey not in self.library.show_rating_key_map:
logger.warning(f"Filter Error: No {'TMDb' if self.library.is_movie else 'TVDb'} ID found for {current.title}")
util.print_return(f"Filtering {display} {item.title}")
if self.tmdb_filters and isinstance(item, (Movie, Show)):
if item.ratingKey not in self.library.movie_rating_key_map and item.ratingKey not in self.library.show_rating_key_map:
logger.warning(f"Filter Error: No {'TMDb' if self.library.is_movie else 'TVDb'} ID found for {item.title}")
return False
try:
if current.ratingKey in self.library.movie_rating_key_map:
t_id = self.library.movie_rating_key_map[current.ratingKey]
if item.ratingKey in self.library.movie_rating_key_map:
t_id = self.library.movie_rating_key_map[item.ratingKey]
else:
t_id = self.library.show_rating_key_map[current.ratingKey]
t_id = self.library.show_rating_key_map[item.ratingKey]
except Failed as e:
logger.error(e)
return False
if not self.check_tmdb_filter(t_id, current.ratingKey in self.library.movie_rating_key_map):
if not self.check_tmdb_filter(t_id, item.ratingKey in self.library.movie_rating_key_map):
return False
for filter_method, filter_data in self.filters:
filter_attr, modifier, filter_final = self._split(filter_method)
filter_actual = filter_translation[filter_attr] if filter_attr in filter_translation else filter_attr
if filter_attr in ["release", "added", "last_played"]:
if util.is_date_filter(getattr(current, filter_actual), modifier, filter_data, filter_final, self.current_time):
item_type = self.collection_level
if self.collection_level == "item":
if isinstance(item, Movie):
item_type = "movie"
elif isinstance(item, Show):
item_type = "show"
elif isinstance(item, Season):
item_type = "season"
elif isinstance(item, Episode):
item_type = "episode"
elif isinstance(item, Artist):
item_type = "artist"
elif isinstance(item, Album):
item_type = "album"
elif isinstance(item, Track):
item_type = "track"
else:
continue
if filter_attr not in filters[item_type]:
continue
elif filter_attr in ["release", "added", "last_played"]:
if util.is_date_filter(getattr(item, filter_actual), modifier, filter_data, filter_final, self.current_time):
return False
elif filter_attr in ["audio_track_title", "filepath", "title", "studio"]:
values = []
if filter_attr == "audio_track_title":
for media in current.media:
for media in item.media:
for part in media.parts:
values.extend([a.title for a in part.audioStreams() if a.title])
elif filter_attr == "filepath":
values = [loc for loc in current.locations]
values = [loc for loc in item.locations]
elif filter_attr in ["title", "studio"]:
values = [getattr(current, filter_actual)]
values = [getattr(item, filter_actual)]
if util.is_string_filter(values, modifier, filter_data):
return False
elif filter_attr in boolean_filters:
filter_check = False
if filter_attr == "has_collection":
filter_check = len(current.collections) > 0
filter_check = len(item.collections) > 0
elif filter_attr == "has_overlay":
for label in current.labels:
for label in item.labels:
if label.tag.lower().endswith(" overlay"):
filter_check = True
if util.is_boolean_filter(filter_data, filter_check):
return False
elif filter_attr == "history":
item_date = current.originallyAvailableAt
item_date = item.originallyAvailableAt
if item_date is None:
return False
elif filter_data == "day":
@ -1733,12 +1815,12 @@ class CollectionBuilder:
return False
elif modifier in [".gt", ".gte", ".lt", ".lte"]:
divider = 60000 if filter_attr == "duration" else 1
if util.is_number_filter(getattr(current, filter_actual) / divider, modifier, filter_data):
if util.is_number_filter(getattr(item, filter_actual) / divider, modifier, filter_data):
return False
else:
attrs = []
if filter_attr in ["resolution", "audio_language", "subtitle_language"]:
for media in current.media:
for media in item.media:
if filter_attr == "resolution":
attrs.extend([media.videoResolution])
for part in media.parts:
@ -1747,16 +1829,16 @@ class CollectionBuilder:
if filter_attr == "subtitle_language":
attrs.extend([s.language for s in part.subtitleStreams()])
elif filter_attr in ["content_rating", "year", "rating"]:
attrs = [str(getattr(current, filter_actual))]
attrs = [str(getattr(item, filter_actual))]
elif filter_attr in ["actor", "country", "director", "genre", "label", "producer", "writer", "collection"]:
attrs = [attr.tag for attr in getattr(current, filter_actual)]
attrs = [attr.tag for attr in getattr(item, filter_actual)]
else:
raise Failed(f"Filter Error: filter: {filter_final} not supported")
if (not list(set(filter_data) & set(attrs)) and modifier == "") \
or (list(set(filter_data) & set(attrs)) and modifier == ".not"):
return False
util.print_return(f"Filtering {display} {current.title}")
util.print_return(f"Filtering {display} {item.title}")
return True
def run_missing(self):
@ -1924,7 +2006,7 @@ class CollectionBuilder:
logger.error(e)
# Locking should come before refreshing since refreshing can change metadata (i.e. if specified to both lock
# background/poster and also refreshing, assume that the current background/poster should be kept)
# background/poster and also refreshing, assume that the item background/poster should be kept)
if "item_lock_background" in self.item_details:
self.library.query(item.lockArt)
if "item_lock_poster" in self.item_details:

@ -9,6 +9,18 @@ logger = logging.getLogger("Plex Meta Manager")
github_base = "https://raw.githubusercontent.com/meisnate12/Plex-Meta-Manager-Configs/master/"
advance_tags_to_edit = {
"Movie": ["metadata_language", "use_original_title"],
"Show": ["episode_sorting", "keep_episodes", "delete_episodes", "season_display", "episode_ordering",
"metadata_language", "use_original_title"],
"Artist": ["album_sort"]
}
tags_to_edit = {
"Movie": ["genre", "label", "collection", "country", "director", "producer", "writer"],
"Show": ["genre", "label", "collection"],
"Artist": ["genre", "style", "mood", "country", "collection", "similar_artist"]
}
def get_dict(attribute, attr_data, check_list=None):
if check_list is None:
@ -19,14 +31,11 @@ def get_dict(attribute, attr_data, check_list=None):
new_dict = {}
for _name, _data in attr_data[attribute].items():
if _name in check_list:
logger.error(
f"Config Warning: Skipping duplicate {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name}")
logger.error(f"Config Warning: Skipping duplicate {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name}")
elif _data is None:
logger.error(
f"Config Warning: {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name} has no data")
logger.error(f"Config Warning: {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name} has no data")
elif not isinstance(_data, dict):
logger.error(
f"Config Warning: {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name} must be a dictionary")
logger.error(f"Config Warning: {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name} must be a dictionary")
else:
new_dict[str(_name)] = _data
return new_dict
@ -221,6 +230,48 @@ class MetadataFile(DataFile):
else:
return self.collections
def edit_tags(self, attr, obj, group, alias, extra=None):
if attr in alias and f"{attr}.sync" in alias:
logger.error(f"Metadata Error: Cannot use {attr} and {attr}.sync together")
elif f"{attr}.remove" in alias and f"{attr}.sync" in alias:
logger.error(f"Metadata Error: Cannot use {attr}.remove and {attr}.sync together")
elif attr in alias and group[alias[attr]] is None:
logger.error(f"Metadata Error: {attr} attribute is blank")
elif f"{attr}.remove" in alias and group[alias[f"{attr}.remove"]] is None:
logger.error(f"Metadata Error: {attr}.remove attribute is blank")
elif f"{attr}.sync" in alias and group[alias[f"{attr}.sync"]] is None:
logger.error(f"Metadata Error: {attr}.sync attribute is blank")
elif attr in alias or f"{attr}.remove" in alias or f"{attr}.sync" in alias:
add_tags = util.get_list(group[alias[attr]]) if attr in alias else []
if extra:
add_tags.extend(extra)
remove_tags = util.get_list(group[alias[f"{attr}.remove"]]) if f"{attr}.remove" in alias else None
sync_tags = util.get_list(group[alias[f"{attr}.sync"]] if group[alias[f"{attr}.sync"]] else []) if f"{attr}.sync" in alias else None
return self.library.edit_tags(attr, obj, add_tags=add_tags, remove_tags=remove_tags, sync_tags=sync_tags)
return False
def set_images(self, obj, group, alias):
def set_image(attr, is_poster=True, is_url=True):
if group[alias[attr]]:
return ImageData(attr, group[alias[attr]], is_poster=is_poster, is_url=is_url)
else:
logger.error(f"Metadata Error: {attr} attribute is blank")
poster = None
background = None
if "url_poster" in alias:
poster = set_image("url_poster")
elif "file_poster" in alias:
poster = set_image("file_poster", is_url=False)
if "url_background" in alias:
background = set_image("url_background", is_poster=False)
elif "file_background" in alias:
background = set_image("file_background",is_poster=False, is_url=False)
if poster or background:
self.library.upload_images(obj, poster=poster, background=background)
def update_metadata(self):
if not self.metadata:
return None
@ -229,8 +280,6 @@ class MetadataFile(DataFile):
logger.info("")
for mapping_name, meta in self.metadata.items():
methods = {mm.lower(): mm for mm in meta}
if self.config.test_mode and ("test" not in methods or meta[methods["test"]] is not True):
continue
updated = False
edits = {}
@ -243,13 +292,11 @@ class MetadataFile(DataFile):
if value is None: value = group[alias[name]]
try:
current = str(getattr(current_item, key, ""))
final_value = None
if var_type == "date":
final_value = util.validate_date(value, name, return_as="%Y-%m-%d")
current = current[:-9]
elif var_type == "float":
if value is None:
raise Failed(f"Metadata Error: {name} attribute is blank")
final_value = None
try:
value = float(str(value))
if 0 <= value <= 10:
@ -258,6 +305,13 @@ class MetadataFile(DataFile):
pass
if final_value is None:
raise Failed(f"Metadata Error: {name} attribute must be a number between 0 and 10")
elif var_type == "int":
try:
final_value = int(str(value))
except ValueError:
pass
if final_value is None:
raise Failed(f"Metadata Error: {name} attribute must be an integer")
else:
final_value = value
if current != str(final_value):
@ -269,13 +323,11 @@ class MetadataFile(DataFile):
else:
logger.error(f"Metadata Error: {name} attribute is blank")
def add_advanced_edit(attr, obj, group, alias, show_library=False, new_agent=False):
def add_advanced_edit(attr, obj, group, alias, new_agent=False):
key, options = plex.item_advance_keys[f"item_{attr}"]
if attr in alias:
if new_agent and self.library.agent not in plex.new_plex_agents:
logger.error(f"Metadata Error: {attr} attribute only works for with the New Plex Movie Agent and New Plex TV Agent")
elif show_library and not self.library.is_show:
logger.error(f"Metadata Error: {attr} attribute only works for show libraries")
elif group[alias[attr]]:
method_data = str(group[alias[attr]]).lower()
if method_data not in options:
@ -286,54 +338,12 @@ class MetadataFile(DataFile):
else:
logger.error(f"Metadata Error: {attr} attribute is blank")
def edit_tags(attr, obj, group, alias, extra=None, movie_library=False):
if movie_library and not self.library.is_movie and (attr in alias or f"{attr}.sync" in alias or f"{attr}.remove" in alias):
logger.error(f"Metadata Error: {attr} attribute only works for movie libraries")
elif attr in alias and f"{attr}.sync" in alias:
logger.error(f"Metadata Error: Cannot use {attr} and {attr}.sync together")
elif f"{attr}.remove" in alias and f"{attr}.sync" in alias:
logger.error(f"Metadata Error: Cannot use {attr}.remove and {attr}.sync together")
elif attr in alias and group[alias[attr]] is None:
logger.error(f"Metadata Error: {attr} attribute is blank")
elif f"{attr}.remove" in alias and group[alias[f"{attr}.remove"]] is None:
logger.error(f"Metadata Error: {attr}.remove attribute is blank")
elif f"{attr}.sync" in alias and group[alias[f"{attr}.sync"]] is None:
logger.error(f"Metadata Error: {attr}.sync attribute is blank")
elif attr in alias or f"{attr}.remove" in alias or f"{attr}.sync" in alias:
add_tags = util.get_list(group[alias[attr]]) if attr in alias else []
if extra:
add_tags.extend(extra)
remove_tags = util.get_list(group[alias[f"{attr}.remove"]]) if f"{attr}.remove" in alias else None
sync_tags = util.get_list(group[alias[f"{attr}.sync"]] if group[alias[f"{attr}.sync"]] else []) if f"{attr}.sync" in alias else None
return self.library.edit_tags(attr, obj, add_tags=add_tags, remove_tags=remove_tags, sync_tags=sync_tags)
return False
def set_image(attr, group, alias, is_poster=True, is_url=True):
if group[alias[attr]]:
return ImageData(attr, group[alias[attr]], is_poster=is_poster, is_url=is_url)
else:
logger.error(f"Metadata Error: {attr} attribute is blank")
def set_images(obj, group, alias):
poster = None
background = None
if "url_poster" in alias:
poster = set_image("url_poster", group, alias)
elif "file_poster" in alias:
poster = set_image("file_poster", group, alias, is_url=False)
if "url_background" in alias:
background = set_image("url_background", group, alias, is_poster=False)
elif "file_background" in alias:
background = set_image("file_background", group, alias, is_poster=False, is_url=False)
if poster or background:
self.library.upload_images(obj, poster=poster, background=background)
logger.info("")
util.separator()
logger.info("")
year = None
if "year" in methods:
if "year" in methods and not self.library.is_music:
next_year = datetime.now().year + 1
if meta[methods["year"]] is None:
raise Failed("Metadata Error: year attribute is blank")
@ -370,15 +380,14 @@ class MetadataFile(DataFile):
logger.error(f"Skipping {mapping_name}")
continue
item_type = "Movie" if self.library.is_movie else "Show"
logger.info(f"Updating {item_type}: {title}...")
logger.info(f"Updating {self.library.type}: {title}...")
tmdb_item = None
tmdb_is_movie = None
if ("tmdb_show" in methods or "tmdb_id" in methods) and "tmdb_movie" in methods:
if not self.library.is_music and ("tmdb_show" in methods or "tmdb_id" in methods) and "tmdb_movie" in methods:
logger.error("Metadata Error: Cannot use tmdb_movie and tmdb_show when editing the same metadata item")
if "tmdb_show" in methods or "tmdb_id" in methods or "tmdb_movie" in methods:
if not self.library.is_music and "tmdb_show" in methods or "tmdb_id" in methods or "tmdb_movie" in methods:
try:
if "tmdb_show" in methods or "tmdb_id" in methods:
data = meta[methods["tmdb_show" if "tmdb_show" in methods else "tmdb_id"]]
@ -421,6 +430,7 @@ class MetadataFile(DataFile):
edits = {}
add_edit("title", item, meta, methods, value=title)
add_edit("sort_title", item, meta, methods, key="titleSort")
if not self.library.is_music:
add_edit("originally_available", item, meta, methods, key="originallyAvailableAt", value=originally_available, var_type="date")
add_edit("critic_rating", item, meta, methods, value=rating, key="rating", var_type="float")
add_edit("audience_rating", item, meta, methods, key="audienceRating", var_type="float")
@ -430,43 +440,42 @@ class MetadataFile(DataFile):
add_edit("studio", item, meta, methods, value=studio)
add_edit("tagline", item, meta, methods, value=tagline)
add_edit("summary", item, meta, methods, value=summary)
if self.library.edit_item(item, mapping_name, item_type, edits):
if self.library.edit_item(item, mapping_name, self.library.type, edits):
updated = True
advance_edits = {}
for advance_edit in ["episode_sorting", "keep_episodes", "delete_episodes", "season_display", "episode_ordering", "metadata_language", "use_original_title"]:
is_show = advance_edit in ["episode_sorting", "keep_episodes", "delete_episodes", "season_display", "episode_ordering"]
for advance_edit in advance_tags_to_edit[self.library.type]:
is_new_agent = advance_edit in ["metadata_language", "use_original_title"]
add_advanced_edit(advance_edit, item, meta, methods, show_library=is_show, new_agent=is_new_agent)
if self.library.edit_item(item, mapping_name, item_type, advance_edits, advanced=True):
add_advanced_edit(advance_edit, item, meta, methods, new_agent=is_new_agent)
if self.library.edit_item(item, mapping_name, self.library.type, advance_edits, advanced=True):
updated = True
for tag_edit in ["genre", "label", "collection", "country", "director", "producer", "writer"]:
is_movie = tag_edit in ["country", "director", "producer", "writer"]
has_extra = genres if tag_edit == "genre" else None
if edit_tags(tag_edit, item, meta, methods, movie_library=is_movie, extra=has_extra):
for tag_edit in tags_to_edit[self.library.type]:
if self.edit_tags(tag_edit, item, meta, methods, extra=genres if tag_edit == "genre" else None):
updated = True
logger.info(f"{item_type}: {mapping_name} Details Update {'Complete' if updated else 'Not Needed'}")
logger.info(f"{self.library.type}: {mapping_name} Details Update {'Complete' if updated else 'Not Needed'}")
set_images(item, meta, methods)
self.set_images(item, meta, methods)
if "seasons" in methods and self.library.is_show:
if meta[methods["seasons"]]:
for season_id in meta[methods["seasons"]]:
if not meta[methods["seasons"]]:
logger.error("Metadata Error: seasons attribute is blank")
elif not isinstance(meta[methods["seasons"]], dict):
logger.error("Metadata Error: seasons attribute must be a dictionary")
else:
for season_id, season_dict in meta[methods["seasons"]].items():
updated = False
logger.info("")
logger.info(f"Updating season {season_id} of {mapping_name}...")
try:
if isinstance(season_id, int):
season = None
for s in item.seasons():
if s.index == season_id:
season = s
break
if season is None:
logger.error(f"Metadata Error: Season: {season_id} not found")
season = item.season(seasson=season_id)
else:
season_dict = meta[methods["seasons"]][season_id]
season = item.season(title=season_id)
except NotFound:
logger.error(f"Metadata Error: Season: {season_id} not found")
continue
season_methods = {sm.lower(): sm for sm in season_dict}
if "title" in season_methods and season_dict[season_methods["title"]]:
@ -488,22 +497,69 @@ class MetadataFile(DataFile):
add_edit("summary", season, season_dict, season_methods)
if self.library.edit_item(season, season_id, "Season", edits):
updated = True
set_images(season, season_dict, season_methods)
else:
logger.error(f"Metadata Error: Season: {season_id} invalid, it must be an integer")
self.set_images(season, season_dict, season_methods)
logger.info(f"Season {season_id} of {mapping_name} Details Update {'Complete' if updated else 'Not Needed'}")
if "episodes" in season_methods and self.library.is_show:
if not season_dict[season_methods["episodes"]]:
logger.error("Metadata Error: episodes attribute is blank")
elif not isinstance(season_dict[season_methods["episodes"]], dict):
logger.error("Metadata Error: episodes attribute must be a dictionary")
else:
logger.error("Metadata Error: seasons attribute is blank")
elif "seasons" in methods:
logger.error("Metadata Error: seasons attribute only works for show libraries")
for episode_str, episode_dict in season_dict[season_methods["episodes"]].items():
updated = False
logger.info("")
logger.info(f"Updating episode {episode_str} in {season_id} of {mapping_name}...")
try:
if isinstance(episode_str, int):
episode = season.episode(episode=episode_str)
else:
episode = season.episode(title=episode_str)
except NotFound:
logger.error(f"Metadata Error: Episode {episode_str} in Season {season_id} not found")
continue
episode_methods = {em.lower(): em for em in episode_dict}
if "title" in episode_methods and episode_dict[episode_methods["title"]]:
title = episode_dict[episode_methods["title"]]
else:
title = episode.title
if "sub" in episode_dict:
if episode_dict[episode_methods["sub"]] is None:
logger.error("Metadata Error: sub attribute is blank")
elif episode_dict[episode_methods["sub"]] is True and "(SUB)" not in title:
title = f"{title} (SUB)"
elif episode_dict[episode_methods["sub"]] is False and title.endswith(" (SUB)"):
title = title[:-6]
else:
logger.error("Metadata Error: sub attribute must be True or False")
edits = {}
add_edit("title", episode, episode_dict, episode_methods, value=title)
add_edit("sort_title", episode, episode_dict, episode_methods, key="titleSort")
add_edit("rating", episode, episode_dict, episode_methods, var_type="float")
add_edit("originally_available", episode, episode_dict, episode_methods, key="originallyAvailableAt", var_type="date")
add_edit("summary", episode, episode_dict, episode_methods)
if self.library.edit_item(episode, f"{episode_str} in Season: {season_id}", "Episode", edits):
updated = True
for tag_edit in ["director", "writer"]:
if self.edit_tags(tag_edit, episode, episode_dict, episode_methods):
updated = True
self.set_images(episode, episode_dict, episode_methods)
logger.info(f"Episode {episode_str} in Season {season_id} of {mapping_name} Details Update {'Complete' if updated else 'Not Needed'}")
if "episodes" in methods and self.library.is_show:
if meta[methods["episodes"]]:
for episode_str in meta[methods["episodes"]]:
if not meta[methods["episodes"]]:
logger.error("Metadata Error: episodes attribute is blank")
elif not isinstance(meta[methods["episodes"]], dict):
logger.error("Metadata Error: episodes attribute must be a dictionary")
else:
for episode_str, episode_dict in meta[methods["episodes"]].items():
updated = False
logger.info("")
match = re.search("[Ss]\\d+[Ee]\\d+", episode_str)
if match:
if not match:
logger.error(f"Metadata Error: episode {episode_str} invalid must have S##E## format")
continue
output = match.group(0)[1:].split("E" if "E" in match.group(0) else "e")
season_id = int(output[0])
episode_id = int(output[1])
@ -512,8 +568,7 @@ class MetadataFile(DataFile):
episode = item.episode(season=season_id, episode=episode_id)
except NotFound:
logger.error(f"Metadata Error: episode {episode_id} of season {season_id} not found")
else:
episode_dict = meta[methods["episodes"]][episode_str]
continue
episode_methods = {em.lower(): em for em in episode_dict}
if "title" in episode_methods and episode_dict[episode_methods["title"]]:
@ -537,18 +592,83 @@ class MetadataFile(DataFile):
add_edit("summary", episode, episode_dict, episode_methods)
if self.library.edit_item(episode, f"{season_id} Episode: {episode_id}", "Season", edits):
updated = True
if edit_tags("director", episode, episode_dict, episode_methods):
updated = True
if edit_tags("writer", episode, episode_dict, episode_methods):
for tag_edit in ["director", "writer"]:
if self.edit_tags(tag_edit, episode, episode_dict, episode_methods):
updated = True
set_images(episode, episode_dict, episode_methods)
self.set_images(episode, episode_dict, episode_methods)
logger.info(f"Episode S{season_id}E{episode_id} of {mapping_name} Details Update {'Complete' if updated else 'Not Needed'}")
if "albums" in methods and self.library.is_music:
if not meta[methods["albums"]]:
logger.error("Metadata Error: albums attribute is blank")
elif not isinstance(meta[methods["albums"]], dict):
logger.error("Metadata Error: albums attribute must be a dictionary")
else:
logger.error(f"Metadata Error: episode {episode_str} invalid must have S##E## format")
for album_name, album_dict in meta[methods["albums"]].items():
updated = False
logger.info("")
logger.info(f"Updating album {album_name} of {mapping_name}...")
try:
album = item.album(album_name)
except NotFound:
logger.error(f"Metadata Error: Album: {album_name} not found")
continue
album_methods = {am.lower(): am for am in album_dict}
if "album" in album_methods and album_dict[album_methods["album"]]:
title = album_dict[album_methods["album"]]
else:
logger.error("Metadata Error: episodes attribute is blank")
elif "episodes" in methods:
logger.error("Metadata Error: episodes attribute only works for show libraries")
title = album.title
edits = {}
add_edit("album", album, album_dict, album_methods, key="title", value=title)
add_edit("sort_album", album, album_dict, album_methods, key="titleSort")
add_edit("rating", album, album_dict, album_methods, var_type="float")
add_edit("originally_available", album, album_dict, album_methods, key="originallyAvailableAt", var_type="date")
add_edit("record_label", album, album_dict, album_methods, key="studio")
add_edit("review", album, album_dict, album_methods, key="summary")
if self.library.edit_item(album, title, "Album", edits):
updated = True
for tag_edit in ["genre", "style", "mood", "collection", "label"]:
if self.edit_tags(tag_edit, album, album_dict, album_methods):
updated = True
self.set_images(album, album_dict, album_methods)
logger.info(f"Album: {title} of {mapping_name} Details Update {'Complete' if updated else 'Not Needed'}")
if "tracks" in album_methods:
if not album_dict[album_methods["tracks"]]:
logger.error("Metadata Error: tracks attribute is blank")
elif not isinstance(album_dict[album_methods["tracks"]], dict):
logger.error("Metadata Error: tracks attribute must be a dictionary")
else:
for track_num, track_dict in album_dict[album_methods["tracks"]].items():
updated = False
logger.info("")
logger.info(f"Updating track {track_num} on {album_name} of {mapping_name}...")
try:
if isinstance(track_num, int):
track = album.track(track=track_num)
else:
track = album.track(title=track_num)
except NotFound:
logger.error(f"Metadata Error: Track: {track_num} not found")
continue
track_methods = {tm.lower(): tm for tm in track_dict}
if "title" in track_methods and track_dict[track_methods["title"]]:
title = track_dict[track_methods["title"]]
else:
title = track.title
edits = {}
add_edit("title", track, track_dict, track_methods, value=title)
add_edit("rating", track, track_dict, track_methods, var_type="float")
add_edit("track", track, track_dict, track_methods, key="index", var_type="int")
add_edit("disc", track, track_dict, track_methods, key="parentIndex", var_type="int")
add_edit("artist", track, track_dict, track_methods, key="originalTitle")
if self.library.edit_item(album, title, "Track", edits):
updated = True
if self.edit_tags("mood", track, track_dict, track_methods):
updated = True
logger.info(f"Track: {track_num} on Album: {title} of {mapping_name} Details Update {'Complete' if updated else 'Not Needed'}")
class PlaylistFile(DataFile):

@ -41,7 +41,47 @@ search_translation = {
"audio_language": "audioLanguage",
"progress": "inProgress",
"episode_progress": "episode.inProgress",
"unplayed_episodes": "show.unwatchedLeaves"
"unplayed_episodes": "show.unwatchedLeaves",
"artist_title": "artist.title",
"artist_user_rating": "artist.userRating",
"artist_genre": "artist.genre",
"artist_collection": "artist.collection",
"artist_country": "artist.country",
"artist_mood": "artist.mood",
"artist_style": "artist.style",
"artist_added": "artist.addedAt",
"artist_last_played": "artist.lastViewedAt",
"artist_unmatched": "artist.unmatched",
"album_title": "album.title",
"album_year": "album.year",
"album_decade": "album.decade",
"album_genre": "album.genre",
"album_plays": "album.viewCount",
"album_last_played": "album.lastViewedAt",
"album_user_rating": "album.userRating",
"album_critic_rating": "album.rating",
"album_record_label": "album.studio",
"album_mood": "album.mood",
"album_style": "album.style",
"album_format": "album.format",
"album_type": "album.subformat",
"album_collection": "album.collection",
"album_added": "album.addedAt",
"album_released": "album.originallyAvailableAt",
"album_unmatched": "album.unmatched",
"album_source": "album.source",
"album_label": "album.label",
"track_mood": "track.mood",
"track_title": "track.title",
"track_plays": "track.viewCount",
"track_last_played": "track.lastViewedAt",
"track_skips": "track.skipCount",
"track_last_skipped": "track.lastSkippedAt",
"track_user_rating": "track.userRating",
"track_last_rated": "track.lastRatedAt",
"track_added": "track.addedAt",
"track_trash": "track.trash",
"track_source": "track.source"
}
show_translation = {
"title": "show.title",
@ -71,6 +111,7 @@ modifier_translation = {
".before": "%3C%3C", ".after": "%3E%3E", ".begins": "%3C", ".ends": "%3E"
}
episode_sorting_options = {"default": "-1", "oldest": "0", "newest": "1"}
album_sorting_options = {"default": -1, "newest": 0, "oldest": 1, "name": 2}
keep_episodes_options = {"all": 0, "5_latest": 5, "3_latest": 3, "latest": 1, "past_3": -3, "past_7": -7, "past_30": -30}
delete_episodes_options = {"never": 0, "day": 1, "week": 7, "refresh": 100}
season_display_options = {"default": -1, "show": 0, "hide": 1}
@ -83,10 +124,13 @@ metadata_language_options = {lang.lower(): lang for lang in plex_languages}
metadata_language_options["default"] = None
use_original_title_options = {"default": -1, "no": 0, "yes": 1}
collection_order_options = ["release", "alpha", "custom"]
collection_level_options = ["episode", "season"]
collection_level_show_options = ["episode", "season"]
collection_level_music_options = ["album", "track"]
collection_level_options = collection_level_show_options + collection_level_music_options
collection_mode_keys = {-1: "default", 0: "hide", 1: "hideItems", 2: "showItems"}
collection_order_keys = {0: "release", 1: "alpha", 2: "custom"}
item_advance_keys = {
"item_album_sorting": ("albumSort", album_sorting_options),
"item_episode_sorting": ("episodeSort", episode_sorting_options),
"item_keep_episodes": ("autoDeletionItemPolicyUnwatchedLibrary", keep_episodes_options),
"item_delete_episodes": ("autoDeletionItemPolicyWatchedLibrary", delete_episodes_options),
@ -96,6 +140,48 @@ item_advance_keys = {
"item_use_original_title": ("useOriginalTitle", use_original_title_options)
}
new_plex_agents = ["tv.plex.agents.movie", "tv.plex.agents.series"]
music_searches = [
"artist_title", "artist_title.not", "artist_title.is", "artist_title.isnot", "artist_title.begins", "artist_title.ends",
"artist_user_rating.gt", "artist_user_rating.gte", "artist_user_rating.lt", "artist_user_rating.lte",
"artist_genre", "artist_genre.not",
"artist_collection", "artist_collection.not",
"artist_country", "artist_country.not",
"artist_mood", "artist_mood.not",
"artist_style", "artist_style.not",
"artist_added", "artist_added.not", "artist_added.before", "artist_added.after",
"artist_last_played", "artist_last_played.not", "artist_last_played.before", "artist_last_played.after",
"artist_unmatched",
"album_title", "album_title.not", "album_title.is", "album_title.isnot", "album_title.begins", "album_title.ends",
"album_year.gt", "album_year.gte", "album_year.lt", "album_year.lte",
"album_decade",
"album_genre", "album_genre.not",
"album_plays.gt", "album_plays.gte", "album_plays.lt", "album_plays.lte",
"album_last_played", "album_last_played.not", "album_last_played.before", "album_last_played.after",
"album_user_rating.gt", "album_user_rating.gte", "album_user_rating.lt", "album_user_rating.lte",
"album_critic_rating.gt", "album_critic_rating.gte", "album_critic_rating.lt", "album_critic_rating.lte",
"album_record_label", "album_record_label.not", "album_record_label.is", "album_record_label.isnot", "album_record_label.begins", "album_record_label.ends",
"album_mood", "album_mood.not",
"album_style", "album_style.not",
"album_format", "album_format.not",
"album_type", "album_type.not",
"album_collection", "album_collection.not",
"album_added", "album_added.not", "album_added.before", "album_added.after",
"album_released", "album_released.not", "album_released.before", "album_released.after",
"album_unmatched",
"album_source", "album_source.not",
"album_label", "album_label.not",
"track_mood", "track_mood.not",
"track_title", "track_title.not", "track_title.is", "track_title.isnot", "track_title.begins", "track_title.ends",
"track_plays.gt", "track_plays.gte", "track_plays.lt", "track_plays.lte",
"track_last_played", "track_last_played.not", "track_last_played.before", "track_last_played.after",
"track_skips.gt", "track_skips.gte", "track_skips.lt", "track_skips.lte",
"track_last_skipped", "track_last_skipped.not", "track_last_skipped.before", "track_last_skipped.after",
"track_user_rating.gt", "track_user_rating.gte", "track_user_rating.lt", "track_user_rating.lte",
"track_last_rated", "track_last_rated.not", "track_last_rated.before", "track_last_rated.after",
"track_added", "track_added.not", "track_added.before", "track_added.after",
"track_trash",
"track_source", "track_source.not"
]
searches = [
"title", "title.not", "title.is", "title.isnot", "title.begins", "title.ends",
"studio", "studio.not", "studio.is", "studio.isnot", "studio.begins", "studio.ends",
@ -129,7 +215,7 @@ searches = [
"episode_plays.gt", "episode_plays.gte", "episode_plays.lt", "episode_plays.lte",
"episode_user_rating.gt", "episode_user_rating.gte", "episode_user_rating.lt", "episode_user_rating.lte",
"episode_year", "episode_year.not", "episode_year.gt", "episode_year.gte", "episode_year.lt", "episode_year.lte"
]
] + music_searches
and_searches = [
"title.and", "studio.and", "actor.and", "audio_language.and", "collection.and",
"content_rating.and", "country.and", "director.and", "genre.and", "label.and",
@ -157,18 +243,29 @@ show_only_searches = [
"episode_year", "episode_year.not", "episode_year.gt", "episode_year.gte", "episode_year.lt", "episode_year.lte",
"unplayed_episodes", "episode_unplayed", "episode_duplicate", "episode_progress", "episode_unmatched",
]
float_attributes = ["user_rating", "episode_user_rating", "critic_rating", "audience_rating"]
string_attributes = ["title", "studio", "episode_title", "artist_title", "album_title", "album_record_label", "track_title"]
float_attributes = [
"user_rating", "episode_user_rating", "critic_rating", "audience_rating",
"artist_user_rating", "album_user_rating", "album_critic_rating", "track_user_rating"
]
boolean_attributes = [
"hdr", "unmatched", "duplicate", "unplayed", "progress", "trash",
"unplayed_episodes", "episode_unplayed", "episode_duplicate", "episode_progress", "episode_unmatched",
"hdr", "unmatched", "duplicate", "unplayed", "progress", "trash", "unplayed_episodes", "episode_unplayed",
"episode_duplicate", "episode_progress", "episode_unmatched", "artist_unmatched", "album_unmatched", "track_trash"
]
tmdb_attributes = ["actor", "director", "producer", "writer"]
date_attributes = ["added", "episode_added", "release", "episode_air_date", "last_played", "episode_last_played", "first_episode_aired", "last_episode_aired"]
number_attributes = ["plays", "episode_plays", "duration", "tmdb_vote_count"] + date_attributes
date_attributes = [
"added", "episode_added", "release", "episode_air_date", "last_played", "episode_last_played",
"first_episode_aired", "last_episode_aired", "artist_added", "artist_last_played", "album_last_played",
"album_added", "album_released", "track_last_played", "track_last_skipped", "track_last_rated", "track_added"
]
year_attributes = ["decade", "year", "episode_year", "album_year", "album_decade"]
number_attributes = ["plays", "episode_plays", "duration", "tmdb_vote_count", "album_plays", "track_plays", "track_skips"] + year_attributes
search_display = {"added": "Date Added", "release": "Release Date", "hdr": "HDR", "progress": "In Progress", "episode_progress": "Episode In Progress"}
tags = [
"actor", "audio_language", "collection", "content_rating", "country", "director", "genre", "label",
"network", "producer", "resolution", "studio", "subtitle_language", "writer"
tag_attributes = [
"actor", "audio_language", "collection", "content_rating", "country", "director", "genre", "label", "network",
"producer", "resolution", "studio", "subtitle_language", "writer" "artist_genre", "artist_collection",
"artist_country", "artist_mood", "artist_style", "album_genre", "album_mood", "album_style", "album_format",
"album_type", "album_collection", "album_source", "album_label", "track_mood", "track_source"
]
movie_sorts = {
"title.asc": "titleSort", "title.desc": "titleSort%3Adesc",
@ -180,8 +277,12 @@ movie_sorts = {
"user_rating.asc": "userRating", "user_rating.desc": "userRating%3Adesc",
"content_rating.asc": "contentRating", "content_rating.desc": "contentRating%3Adesc",
"duration.asc": "duration", "duration.desc": "duration%3Adesc",
"progress.asc": "viewOffset", "progress.desc": "viewOffset%3Adesc",
"plays.asc": "viewCount", "plays.desc": "viewCount%3Adesc",
"added.asc": "addedAt", "added.desc": "addedAt%3Adesc",
"viewed.asc": "lastViewedAt", "viewed.desc": "lastViewedAt%3Adesc",
"resolution.asc": "mediaHeight", "resolution.desc": "mediaHeight%3Adesc",
"bitrate.asc": "mediaBitrate", "bitrate.desc": "mediaBitrate%3Adesc",
"random": "random"
}
show_sorts = {
@ -193,8 +294,10 @@ show_sorts = {
"audience_rating.asc": "audienceRating", "audience_rating.desc": "audienceRating%3Adesc",
"user_rating.asc": "userRating", "user_rating.desc": "userRating%3Adesc",
"content_rating.asc": "contentRating", "content_rating.desc": "contentRating%3Adesc",
"added.asc": "addedAt", "added.desc": "addedAt%3Adesc",
"unplayed.asc": "unviewedLeafCount", "unplayed.desc": "unviewedLeafCount%3Adesc",
"episode_added.asc": "episode.addedAt", "episode_added.desc": "episode.addedAt%3Adesc",
"added.asc": "addedAt", "added.desc": "addedAt%3Adesc",
"viewed.asc": "lastViewedAt", "viewed.desc": "lastViewedAt%3Adesc",
"random": "random"
}
season_sorts = {
@ -215,11 +318,61 @@ episode_sorts = {
"audience_rating.asc": "audienceRating", "audience_rating.desc": "audienceRating%3Adesc",
"user_rating.asc": "userRating", "user_rating.desc": "userRating%3Adesc",
"duration.asc": "duration", "duration.desc": "duration%3Adesc",
"progress.asc": "viewOffset", "progress.desc": "viewOffset%3Adesc",
"plays.asc": "viewCount", "plays.desc": "viewCount%3Adesc",
"added.asc": "addedAt", "added.desc": "addedAt%3Adesc",
"viewed.asc": "lastViewedAt", "viewed.desc": "lastViewedAt%3Adesc",
"resolution.asc": "mediaHeight", "resolution.desc": "mediaHeight%3Adesc",
"bitrate.asc": "mediaBitrate", "bitrate.desc": "mediaBitrate%3Adesc",
"random": "random"
}
artist_sorts = {
"title.asc": "titleSort", "title.desc": "titleSort%3Adesc",
"user_rating.asc": "userRating", "user_rating.desc": "userRating%3Adesc",
"added.asc": "addedAt", "added.desc": "addedAt%3Adesc",
"played.asc": "lastViewedAt", "played.desc": "lastViewedAt%3Adesc",
"plays.asc": "viewCount", "plays.desc": "viewCount%3Adesc",
"random": "random"
}
sort_types = {"movies": (1, movie_sorts), "shows": (2, show_sorts), "seasons": (3, season_sorts), "episodes": (4, episode_sorts)}
album_sorts = {
"title.asc": "titleSort", "title.desc": "titleSort%3Adesc",
"album_artist.asc": "artist.titleSort%2Calbum.titleSort%2Calbum.index%2Calbum.id%2Calbum.originallyAvailableAt",
"album_artist.desc": "artist.titleSort%3Adesc%2Calbum.titleSort%2Calbum.index%2Calbum.id%2Calbum.originallyAvailableAt",
"originally_available.asc": "originallyAvailableAt", "originally_available.desc": "originallyAvailableAt%3Adesc",
"release.asc": "originallyAvailableAt", "release.desc": "originallyAvailableAt%3Adesc",
"critic_rating.asc": "rating", "critic_rating.desc": "rating%3Adesc",
"audience_rating.asc": "audienceRating", "audience_rating.desc": "audienceRating%3Adesc",
"user_rating.asc": "userRating", "user_rating.desc": "userRating%3Adesc",
"added.asc": "addedAt", "added.desc": "addedAt%3Adesc",
"played.asc": "lastViewedAt", "played.desc": "lastViewedAt%3Adesc",
"plays.asc": "viewCount", "plays.desc": "viewCount%3Adesc",
"random": "random"
}
track_sorts = {
"title.asc": "titleSort", "title.desc": "titleSort%3Adesc",
"album_artist.asc": "artist.titleSort%2Calbum.titleSort%2Calbum.year%2Ctrack.absoluteIndex%2Ctrack.index%2Ctrack.titleSort%2Ctrack.id",
"album_artist.desc": "artist.titleSort%3Adesc%2Calbum.titleSort%2Calbum.year%2Ctrack.absoluteIndex%2Ctrack.index%2Ctrack.titleSort%2Ctrack.id",
"artist.asc": "originalTitle", "artist.desc": "originalTitle%3Adesc",
"album.asc": "album.titleSort", "album.desc": "album.titleSort%3Adesc",
"user_rating.asc": "userRating", "user_rating.desc": "userRating%3Adesc",
"duration.asc": "duration", "duration.desc": "duration%3Adesc",
"plays.asc": "viewCount", "plays.desc": "viewCount%3Adesc",
"added.asc": "addedAt", "added.desc": "addedAt%3Adesc",
"played.asc": "lastViewedAt", "played.desc": "lastViewedAt%3Adesc",
"rated.asc": "lastRatedAt", "rated.desc": "lastRatedAt%3Adesc",
"popularity.asc": "ratingCount", "popularity.desc": "ratingCount%3Adesc",
"bitrate.asc": "mediaBitrate", "bitrate.desc": "mediaBitrate%3Adesc",
"random": "random"
}
sort_types = {
"movies": (1, movie_sorts),
"shows": (2, show_sorts),
"seasons": (3, season_sorts),
"episodes": (4, episode_sorts),
"artists": (8, artist_sorts),
"albums": (9, album_sorts),
"tracks": (10, track_sorts)
}
class Plex(Library):
def __init__(self, config, params):
@ -247,7 +400,7 @@ class Plex(Library):
break
if not self.Plex:
raise Failed(f"Plex Error: Plex Library {params['name']} not found. Options: {library_names}")
if self.Plex.type in ["movie", "show"]:
if self.Plex.type in ["movie", "show", "artist"]:
self.type = self.Plex.type.capitalize()
else:
raise Failed(f"Plex Error: Plex Library must be a Movies or TV Shows library")
@ -256,6 +409,7 @@ class Plex(Library):
self.agent = self.Plex.agent
self.is_movie = self.type == "Movie"
self.is_show = self.type == "Show"
self.is_music = self.type == "Artist"
self.is_other = self.agent == "com.plexapp.agents.none"
if self.is_other:
self.type = "Video"
@ -395,6 +549,8 @@ class Plex(Library):
names.append(choice.title)
if choice.key not in names:
names.append(choice.key)
choices[choice.title] = choice.title if title else choice.key
choices[choice.key] = choice.title if title else choice.key
choices[choice.title.lower()] = choice.title if title else choice.key
choices[choice.key.lower()] = choice.title if title else choice.key
return choices, names
@ -636,6 +792,8 @@ class Plex(Library):
def edit_tags(self, attr, obj, add_tags=None, remove_tags=None, sync_tags=None):
display = ""
key = builder.filter_translation[attr] if attr in builder.filter_translation else attr
attr_display = attr.replace("_", " ").title()
attr_call = attr_display.replace(" ", "")
if add_tags or remove_tags or sync_tags is not None:
_add_tags = add_tags if add_tags else []
_remove_tags = [t.lower() for t in remove_tags] if remove_tags else []
@ -648,13 +806,13 @@ class Plex(Library):
_add = [f"{t[:1].upper()}{t[1:]}" for t in _add_tags + _sync_tags if t.lower() not in _item_tags]
_remove = [t for t in _item_tags if (sync_tags is not None and t not in _sync_tags) or t in _remove_tags]
if _add:
self.query_data(getattr(obj, f"add{attr.capitalize()}"), _add)
self.query_data(getattr(obj, f"add{attr_call}"), _add)
display += f"+{', +'.join(_add)}"
if _remove:
self.query_data(getattr(obj, f"remove{attr.capitalize()}"), _remove)
self.query_data(getattr(obj, f"remove{attr_call}"), _remove)
display += f"-{', -'.join(_remove)}"
if len(display) > 0:
logger.info(f"{obj.title[:25]:<25} | {attr.capitalize()} | {display}")
logger.info(f"{obj.title[:25]:<25} | {attr_display} | {display}")
return len(display) > 0
def find_assets(self, item, name=None, upload=True, overlay=None, folders=None, create=None):

@ -2,6 +2,7 @@ import glob, logging, os, re, signal, sys, time, traceback
from datetime import datetime, timedelta
from logging.handlers import RotatingFileHandler
from pathvalidate import is_valid_filename, sanitize_filename
from plexapi.audio import Artist, Album, Track
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
from plexapi.video import Season, Episode, Movie
@ -261,6 +262,10 @@ def item_title(item):
return f"{text}: {item.parentTitle}: {item.title}"
elif isinstance(item, Movie) and item.year:
return f"{item.title} ({item.year})"
elif isinstance(item, Album):
return f"{item.parentTitle}: {item.title}"
elif isinstance(item, Track):
return f"{item.grandparentTitle}: {item.parentTitle}: {item.title}"
else:
return item.title

@ -246,7 +246,7 @@ def update_libraries(config):
logger.debug(f"Optimize: {library.optimize}")
logger.debug(f"Timeout: {library.timeout}")
if not library.is_other:
if not library.is_other and not library.is_music:
logger.info("")
util.separator(f"Mapping {library.name} Library", space=False, border=False)
logger.info("")
@ -776,7 +776,6 @@ def run_collection(config, library, metadata, requested_collections):
if builder.run_again and (len(builder.run_again_movies) > 0 or len(builder.run_again_shows) > 0):
library.run_again.append(builder)
except NotScheduled as e:
util.print_multiline(e, info=True)
except Failed as e:

Loading…
Cancel
Save