Merge pull request #368 from meisnate12/develop

v1.12.1
pull/384/head v1.12.1
meisnate12 3 years ago committed by GitHub
commit e6fd8bf92c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -14,7 +14,7 @@ mod_searches = [
"episodes.gt", "episodes.gte", "episodes.lt", "episodes.lte", "duration.gt", "duration.gte", "duration.lt", "duration.lte",
"score.gt", "score.gte", "score.lt", "score.lte", "popularity.gt", "popularity.gte", "popularity.lt", "popularity.lte"
]
no_mod_searches = ["search", "season", "year", "adult", "min_tag_percent"]
no_mod_searches = ["search", "season", "year", "adult", "min_tag_percent", "limit", "sort_by"]
searches = mod_searches + no_mod_searches
search_types = {
"search": "String", "season": "MediaSeason", "seasonYear": "Int", "isAdult": "Boolean", "minimumTagRank": "Int",
@ -101,6 +101,8 @@ class AniList:
final = ani_attr if attr in no_mod_searches else f"{ani_attr}_{mod_translation[mod]}"
if attr in ["start", "end"]:
value = int(util.validate_date(value, f"anilist_search {key}", return_as="%Y%m%d"))
elif attr in ["season", "format", "status", "genre", "tag", "tag_category"]:
value = self.options[attr.replace("_", " ").title()][value.lower().replace(" / ", "-").replace(" ", "-")]
if mod == "gte":
value -= 1
elif mod == "lte":
@ -178,7 +180,7 @@ class AniList:
for d in util.get_list(data):
data_check = d.lower().replace(" / ", "-").replace(" ", "-")
if data_check in self.options[name]:
valid.append(self.options[name][data_check])
valid.append(d)
if len(valid) > 0:
return valid
raise Failed(f"AniList Error: {name}: {data} does not exist\nOptions: {', '.join([v for k, v in self.options[name].items()])}")

@ -4,7 +4,7 @@ from modules import anidb, anilist, icheckmovies, imdb, letterboxd, mal, plex, r
from modules.util import Failed, ImageData
from PIL import Image
from plexapi.exceptions import BadRequest, NotFound
from plexapi.video import Movie, Show
from plexapi.video import Movie, Show, Season, Episode
from urllib.parse import quote
logger = logging.getLogger("Plex Meta Manager")
@ -40,7 +40,8 @@ method_alias = {
"show_title": "title",
"seasonyear": "year", "isadult": "adult", "startdate": "start", "enddate": "end", "averagescore": "score",
"minimum_tag_percentage": "min_tag_percent", "minimumtagrank": "min_tag_percent", "minimum_tag_rank": "min_tag_percent",
"anilist_tag": "anilist_search", "anilist_genre": "anilist_search", "anilist_season": "anilist_search"
"anilist_tag": "anilist_search", "anilist_genre": "anilist_search", "anilist_season": "anilist_search",
"mal_producer": "mal_studio", "mal_licensor": "mal_studio"
}
filter_translation = {
"actor": "actors",
@ -63,7 +64,7 @@ filter_translation = {
modifier_alias = {".greater": ".gt", ".less": ".lt"}
all_builders = anidb.builders + anilist.builders + icheckmovies.builders + imdb.builders + letterboxd.builders + \
mal.builders + plex.builders + stevenlu.builders + tautulli.builders + tmdb.builders + trakt.builders + tvdb.builders
show_only_builders = ["tmdb_network", "tmdb_show", "tmdb_show_details", "tvdb_show", "tvdb_show_details"]
show_only_builders = ["tmdb_network", "tmdb_show", "tmdb_show_details", "tvdb_show", "tvdb_show_details", "collection_level"]
movie_only_builders = [
"letterboxd_list", "letterboxd_list_details", "icheckmovies_list", "icheckmovies_list_details", "stevenlu_popular",
"tmdb_collection", "tmdb_collection_details", "tmdb_movie", "tmdb_movie_details", "tmdb_now_playing",
@ -75,15 +76,21 @@ summary_details = [
]
poster_details = ["url_poster", "tmdb_poster", "tmdb_profile", "tvdb_poster", "file_poster"]
background_details = ["url_background", "tmdb_background", "tvdb_background", "file_background"]
boolean_details = ["visible_library", "visible_home", "visible_shared", "show_filtered", "show_missing", "save_missing", "item_assets", "missing_only_released"]
boolean_details = ["visible_library", "visible_home", "visible_shared", "show_filtered", "show_missing", "save_missing", "item_assets", "missing_only_released", "revert_overlay"]
string_details = ["sort_title", "content_rating", "name_mapping"]
ignored_details = ["smart_filter", "smart_label", "smart_url", "run_again", "schedule", "sync_mode", "template", "test", "tmdb_person", "build_collection", "collection_order", "validate_builders"]
details = ["collection_mode", "collection_order", "label"] + boolean_details + string_details
ignored_details = [
"smart_filter", "smart_label", "smart_url", "run_again", "schedule", "sync_mode", "template", "test",
"tmdb_person", "build_collection", "collection_order", "collection_level", "validate_builders"
]
details = ["collection_mode", "collection_order", "collection_level", "label"] + boolean_details + string_details
collectionless_details = ["collection_order", "plex_collectionless", "label", "label_sync_mode", "test"] + \
poster_details + background_details + summary_details + string_details
item_details = ["item_label", "item_radarr_tag", "item_sonarr_tag", "item_overlay"] + list(plex.item_advance_keys.keys())
radarr_details = ["radarr_add", "radarr_add_existing", "radarr_folder", "radarr_monitor", "radarr_search", "radarr_availability", "radarr_quality", "radarr_tag"]
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"]
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"
]
all_filters = [
"actor", "actor.not",
"audio_language", "audio_language.not",
@ -129,7 +136,7 @@ movie_only_filters = [
"writer", "writer.not"
]
show_only_filters = ["first_episode_aired", "last_episode_aired", "network"]
smart_invalid = ["collection_order"]
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 = [
"tmdb_list", "tmdb_popular", "tmdb_now_playing", "tmdb_top_rated",
@ -139,8 +146,12 @@ custom_sort_builders = [
"tautulli_popular", "tautulli_watched", "letterboxd_list", "icheckmovies_list",
"anilist_top_rated", "anilist_popular", "anilist_season", "anilist_studio", "anilist_genre", "anilist_tag", "anilist_search",
"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_producer"
"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",
"visible_home", "visible_shared", "show_missing", "save_missing", "missing_only_released"
] + summary_details + poster_details + background_details + string_details
class CollectionBuilder:
def __init__(self, config, library, metadata, name, no_missing, data):
@ -164,6 +175,7 @@ class CollectionBuilder:
self.sonarr_details = {}
self.missing_movies = []
self.missing_shows = []
self.missing_parts = []
self.builders = []
self.filters = []
self.tmdb_filters = []
@ -410,6 +422,21 @@ class CollectionBuilder:
else:
raise Failed(f"Collection 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.collection_level = "movie" if self.library.is_movie else "show"
if "collection_level" in methods:
logger.debug("")
logger.debug("Validating Method: collection_level")
if self.data[methods["collection_level"]] is None:
raise Failed(f"Collection Warning: 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:
self.collection_level = self.data[methods["collection_level"]].lower()
else:
raise Failed(f"Collection 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"]
self.media_type = self.collection_level.capitalize()
if "tmdb_person" in methods:
logger.debug("")
logger.debug("Validating Method: tmdb_person")
@ -478,6 +505,8 @@ class CollectionBuilder:
cant_interact("smart_url", "collectionless")
cant_interact("smart_url", "run_again")
cant_interact("smart_label_collection", "smart_url", fail=True)
cant_interact("smart_label_collection", "parts_collection", fail=True)
cant_interact("smart_url", "parts_collection", fail=True)
self.smart = self.smart_url or self.smart_label_collection
@ -500,6 +529,7 @@ class CollectionBuilder:
elif self.library.is_show and method_name in movie_only_builders: raise Failed(f"Collection 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"Collection 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"Collection 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"Collection Error: {method_final} attribute does not work with Collection Level: {self.details['collection_level'].capitalize()}")
elif self.smart and method_name in smart_invalid: raise Failed(f"Collection Error: {method_final} attribute only works with normal collections")
elif self.collectionless and method_name not in collectionless_details: raise Failed(f"Collection 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"Collection Error: {method_final} builder not allowed when using smart_filter")
@ -763,9 +793,7 @@ class CollectionBuilder:
new_dictionary = {}
for search_method, search_data in dict_data.items():
search_attr, modifier, search_final = self._split(search_method)
if search_data is None:
raise Failed(f"Collection Error: {method_name} {search_final} attribute is blank")
elif search_final not in anilist.searches:
if search_final not in anilist.searches:
raise Failed(f"Collection Error: {method_name} {search_final} attribute not supported")
elif search_attr == "season":
new_dictionary[search_attr] = util.parse(search_attr, search_data, parent=method_name, default=current_season, options=util.seasons)
@ -777,6 +805,8 @@ class CollectionBuilder:
if "season" not in dict_methods:
logger.warning(f"Collection Warning: {method_name} season attribute not found using this season: {current_season} by default")
new_dictionary["season"] = current_season
elif search_data is None:
raise Failed(f"Collection Error: {method_name} {search_final} attribute is blank")
elif search_attr == "adult":
new_dictionary[search_attr] = util.parse(search_attr, search_data, datatype="bool", parent=method_name)
elif search_attr in ["episodes", "duration", "score", "popularity"]:
@ -791,7 +821,7 @@ class CollectionBuilder:
new_dictionary[search_attr] = str(search_data)
elif search_final not in ["sort_by", "limit"]:
raise Failed(f"Collection Error: {method_name} {search_final} attribute not supported")
if len(new_dictionary) > 0:
if len(new_dictionary) == 0:
raise Failed(f"Collection Error: {method_name} must have at least one valid search option")
new_dictionary["sort_by"] = util.parse("sort_by", dict_data, methods=dict_methods, parent=method_name, default="score", options=["score", "popular"])
new_dictionary["limit"] = util.parse("limit", dict_data, datatype="int", methods=dict_methods, default=0, parent=method_name)
@ -829,7 +859,7 @@ class CollectionBuilder:
for mal_id in util.get_int_list(method_data, "MyAnimeList ID"):
self.builders.append((method_name, mal_id))
elif method_name in ["mal_all", "mal_airing", "mal_upcoming", "mal_tv", "mal_ova", "mal_movie", "mal_special", "mal_popular", "mal_favorite", "mal_suggested"]:
self.builders.append((method_name, util.parse(method_name, method_data, datatype="int", default=10)))
self.builders.append((method_name, util.parse(method_name, method_data, datatype="int", default=10, maximum=100 if method_name == "mal_suggested" else 500)))
elif method_name in ["mal_season", "mal_userlist"]:
for dict_data, dict_methods in util.parse(method_name, method_data, datatype="dictlist"):
if method_name == "mal_season":
@ -850,7 +880,7 @@ class CollectionBuilder:
"sort_by": util.parse("sort_by", dict_data, methods=dict_methods, parent=method_name, default="score", options=mal.userlist_sort_options, translation=mal.userlist_sort_translation),
"limit": util.parse("limit", dict_data, datatype="int", methods=dict_methods, default=100, parent=method_name, maximum=1000)
}))
elif method_name in ["mal_genre", "mal_producer"]:
elif method_name in ["mal_genre", "mal_studio"]:
id_name = f"{method_name[4:]}_id"
final_data = []
for data in util.get_list(method_data):
@ -868,7 +898,8 @@ class CollectionBuilder:
for dict_data, dict_methods in util.parse(method_name, method_data, datatype="dictlist"):
new_dictionary = {}
if method_name == "plex_search":
new_dictionary = self.build_filter("plex_search", dict_data)
type_override = f"{self.collection_level}s" if self.collection_level in plex.collection_level_options else None
new_dictionary = self.build_filter("plex_search", dict_data, type_override=type_override)
elif method_name == "plex_collectionless":
prefix_list = util.parse("exclude_prefix", dict_data, datatype="list", methods=dict_methods)
exact_list = util.parse("exclude", dict_data, datatype="list", methods=dict_methods)
@ -1069,12 +1100,12 @@ class CollectionBuilder:
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 == "tmdb":
if id_type == "tmdb" and not self.parts_collection:
if input_id in self.library.movie_map:
rating_keys.append(self.library.movie_map[input_id][0])
elif input_id not in self.missing_movies:
self.missing_movies.append(input_id)
elif id_type in ["tvdb", "tmdb_show"]:
elif id_type in ["tvdb", "tmdb_show"] and not self.parts_collection:
if id_type == "tmdb_show":
try:
input_id = self.config.Convert.tmdb_to_tvdb(input_id, fail=True)
@ -1085,7 +1116,7 @@ class CollectionBuilder:
rating_keys.append(self.library.show_map[input_id][0])
elif input_id not in self.missing_shows:
self.missing_shows.append(input_id)
elif id_type == "imdb":
elif id_type == "imdb" and not self.parts_collection:
if input_id in self.library.imdb_map:
rating_keys.append(self.library.imdb_map[input_id][0])
else:
@ -1102,6 +1133,28 @@ class CollectionBuilder:
except Failed as e:
logger.error(e)
continue
elif id_type == "tvdb_season" and self.collection_level == "season":
show_id, season_num = input_id.split("_")
if int(show_id) in self.library.show_map:
show_item = self.library.fetchItem(self.library.show_map[int(show_id)][0])
try:
episode_item = show_item.season(season=int(season_num))
rating_keys.append(episode_item.ratingKey)
except NotFound:
self.missing_parts.append(f"{show_item.title} Season: {season_num} Missing")
elif int(show_id) not in self.missing_shows:
self.missing_shows.append(int(show_id))
elif id_type == "tvdb_episode" and self.collection_level == "episode":
show_id, season_num, episode_num = input_id.split("_")
if int(show_id) in self.library.show_map:
show_item = self.library.fetchItem(self.library.show_map[int(show_id)][0])
try:
episode_item = show_item.episode(season=int(season_num), episode=int(episode_num))
rating_keys.append(episode_item.ratingKey)
except NotFound:
self.missing_parts.append(f"{show_item.title} Season: {season_num} Episode: {episode_num} Missing")
elif int(show_id) not in self.missing_shows:
self.missing_shows.append(int(show_id))
util.print_end()
if len(rating_keys) > 0:
@ -1124,7 +1177,7 @@ class CollectionBuilder:
except Failed as e:
logger.error(e)
continue
current_title = f"{current.title} ({current.year})" if current.year else current.title
current_title = self.item_title(current)
if self.check_filters(current, f"{(' ' * (max_length - len(str(i))))}{i}/{total}"):
self.rating_keys.append(key)
else:
@ -1132,7 +1185,7 @@ class CollectionBuilder:
if self.details["show_filtered"] is True:
logger.info(f"{name} Collection | X | {current_title}")
def build_filter(self, method, plex_filter, smart=False):
def build_filter(self, method, plex_filter, smart=False, type_override=None):
if smart:
logger.info("")
logger.info(f"Validating Method: {method}")
@ -1148,7 +1201,9 @@ class CollectionBuilder:
if "any" in filter_alias and "all" in filter_alias:
raise Failed(f"Collection Error: Cannot have more then one base")
if smart and "type" in filter_alias and self.library.is_show:
if type_override:
sort_type = type_override
elif smart and "type" in filter_alias and self.library.is_show:
if plex_filter[filter_alias["type"]] not in ["shows", "seasons", "episodes"]:
raise Failed(f"Collection Error: type: {plex_filter[filter_alias['type']]} is invalid, must be either shows, season, or episodes")
sort_type = plex_filter[filter_alias["type"]]
@ -1387,8 +1442,8 @@ class CollectionBuilder:
def fetch_item(self, item):
try:
current = self.library.fetchItem(item.ratingKey if isinstance(item, (Movie, Show)) else int(item))
if not isinstance(current, (Movie, Show)):
current = self.library.fetchItem(item.ratingKey if isinstance(item, (Movie, Show, Season, Episode)) else int(item))
if not isinstance(current, (Movie, Show, Season, Episode)):
raise NotFound
return current
except (BadRequest, NotFound):
@ -1403,19 +1458,17 @@ class CollectionBuilder:
except Failed as e:
logger.error(e)
continue
current_title = f"{current.title} ({current.year})" if current.year else current.title
current_operation = "=" if current in collection_items else "+"
logger.info(util.adjust_space(f"{name} Collection | {current_operation} | {current_title}"))
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
elif self.smart_label_collection:
self.library.query_data(current.addLabel, name)
else:
self.library.query_data(current.addCollection, name)
media_type = f"{'Movie' if self.library.is_movie else 'Show'}{'s' if total > 1 else ''}"
util.print_end()
logger.info("")
logger.info(f"{total} {media_type} Processed")
logger.info(f"{total} {self.collection_level.capitalize()}{'s' if total > 1 else ''} Processed")
def check_tmdb_filter(self, item_id, is_movie, item=None, check_released=False):
if self.tmdb_filters or check_released:
@ -1609,6 +1662,26 @@ class CollectionBuilder:
logger.error(e)
if self.run_again:
self.run_again_shows.extend(missing_tvdb_ids)
if len(self.missing_parts) > 0 and self.library.is_show and self.details["save_missing"] is True:
for missing in self.missing_parts:
logger.info(f"{self.name} Collection | X | {missing}")
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 sync_collection(self):
count_removed = 0
@ -1619,7 +1692,7 @@ class CollectionBuilder:
util.separator(f"Removed from {self.name} Collection", space=False, border=False)
logger.info("")
self.library.reload(item)
logger.info(f"{self.name} Collection | - | {item.title}")
logger.info(f"{self.name} Collection | - | {self.item_title(item)}")
if self.smart_label_collection:
self.library.query_data(item.removeLabel, self.name)
else:
@ -1627,7 +1700,7 @@ class CollectionBuilder:
count_removed += 1
if count_removed > 0:
logger.info("")
logger.info(f"{count_removed} {'Movie' if self.library.is_movie else 'Show'}{'s' if count_removed == 1 else ''} Removed")
logger.info(f"{count_removed} {self.collection_level.capitalize()}{'s' if count_removed == 1 else ''} Removed")
def update_item_details(self):
add_tags = self.item_details["item_label"] if "item_label" in self.item_details else None
@ -1675,10 +1748,14 @@ class CollectionBuilder:
temp_image = os.path.join(overlay_folder, f"temp.png")
overlay = (overlay_name, overlay_folder, overlay_image, temp_image)
revert = "revert_overlay" in self.details and self.details["revert_overlay"]
if revert:
overlay = None
tmdb_ids = []
tvdb_ids = []
for item in items:
if int(item.ratingKey) in rating_keys:
if int(item.ratingKey) in rating_keys and not revert:
rating_keys.remove(int(item.ratingKey))
if self.details["item_assets"] or overlay is not None:
try:
@ -1696,7 +1773,7 @@ class CollectionBuilder:
key, options = plex.item_advance_keys[method_name]
if getattr(item, key) != options[method_data]:
advance_edits[key] = options[method_data]
self.library.edit_item(item, item.title, "Movie" if self.library.is_movie else "Show", advance_edits, advanced=True)
self.library.edit_item(item, item.title, self.collection_level.capitalize(), advance_edits, advanced=True)
if len(tmdb_ids) > 0:
if "item_radarr_tag" in self.item_details:
@ -1889,7 +1966,8 @@ class CollectionBuilder:
logger.debug(keys)
logger.debug(self.rating_keys)
for key in self.rating_keys:
logger.info(f"Moving {keys[key].title} {'after {}'.format(keys[previous].title) if previous else 'to the beginning'}")
text = f"after {self.item_title(keys[previous])}" if previous else "to the beginning"
logger.info(f"Moving {self.item_title(keys[key])} {text}")
self.library.move_item(self.obj, key, after=previous)
previous = key
@ -1911,13 +1989,12 @@ class CollectionBuilder:
except (BadRequest, NotFound):
logger.error(f"Plex Error: Item {rating_key} not found")
continue
current_title = f"{current.title} ({current.year})" if current.year else current.title
if current in collection_items:
logger.info(f"{name} Collection | = | {current_title}")
logger.info(f"{name} Collection | = | {self.item_title(current)}")
else:
self.library.query_data(current.addLabel if self.smart_label_collection else current.addCollection, name)
logger.info(f"{name} Collection | + | {current_title}")
logger.info(f"{len(rating_keys)} {'Movie' if self.library.is_movie else 'Show'}{'s' if len(rating_keys) > 1 else ''} Processed")
logger.info(f"{name} Collection | + | {self.item_title(current)}")
logger.info(f"{len(rating_keys)} {self.collection_level.capitalize()}{'s' if len(rating_keys) > 1 else ''} Processed")
if len(self.run_again_movies) > 0:
logger.info("")

@ -47,7 +47,7 @@ class Config:
yaml.YAML().allow_duplicate_keys = True
try:
new_config, ind, bsi = yaml.util.load_yaml_guess_indent(open(self.config_path, encoding="utf-8"))
new_config, _, _ = yaml.util.load_yaml_guess_indent(open(self.config_path, encoding="utf-8"))
def replace_attr(all_data, attr, par):
if "settings" not in all_data:
all_data["settings"] = {}
@ -90,7 +90,7 @@ class Config:
if "trakt" in new_config: new_config["trakt"] = new_config.pop("trakt")
if "mal" in new_config: new_config["mal"] = new_config.pop("mal")
if "anidb" in new_config: new_config["anidb"] = new_config.pop("anidb")
yaml.round_trip_dump(new_config, open(self.config_path, "w", encoding="utf-8"), indent=ind, block_seq_indent=bsi)
yaml.round_trip_dump(new_config, open(self.config_path, "w", encoding="utf-8"), indent=None, block_seq_indent=2)
self.data = new_config
except yaml.scanner.ScannerError as e:
raise Failed(f"YAML Error: {util.tab_new_lines(e)}")
@ -111,12 +111,12 @@ class Config:
if data is None or attribute not in data:
message = f"{text} not found"
if parent and save is True:
loaded_config, ind_in, bsi_in = yaml.util.load_yaml_guess_indent(open(self.config_path))
loaded_config, _, _ = yaml.util.load_yaml_guess_indent(open(self.config_path))
endline = f"\n{parent} sub-attribute {attribute} added to config"
if parent not in loaded_config or not loaded_config[parent]: loaded_config[parent] = {attribute: default}
elif attribute not in loaded_config[parent]: loaded_config[parent][attribute] = default
else: endline = ""
yaml.round_trip_dump(loaded_config, open(self.config_path, "w"), indent=ind_in, block_seq_indent=bsi_in)
yaml.round_trip_dump(loaded_config, open(self.config_path, "w"), indent=None, block_seq_indent=2)
elif data[attribute] is None:
if default_is_none is True: return None
else: message = f"{text} is blank"

@ -7,7 +7,7 @@ logger = logging.getLogger("Plex Meta Manager")
builders = [
"mal_id", "mal_all", "mal_airing", "mal_upcoming", "mal_tv", "mal_ova", "mal_movie", "mal_special",
"mal_popular", "mal_favorite", "mal_season", "mal_suggested", "mal_userlist", "mal_genre", "mal_producer"
"mal_popular", "mal_favorite", "mal_season", "mal_suggested", "mal_userlist", "mal_genre", "mal_studio"
]
mal_ranked_name = {
"mal_all": "all", "mal_airing": "airing", "mal_upcoming": "upcoming", "mal_tv": "tv", "mal_ova": "ova",
@ -17,7 +17,7 @@ mal_ranked_pretty = {
"mal_all": "MyAnimeList All", "mal_airing": "MyAnimeList Airing",
"mal_upcoming": "MyAnimeList Upcoming", "mal_tv": "MyAnimeList TV", "mal_ova": "MyAnimeList OVA",
"mal_movie": "MyAnimeList Movie", "mal_special": "MyAnimeList Special", "mal_popular": "MyAnimeList Popular",
"mal_favorite": "MyAnimeList Favorite", "mal_genre": "MyAnimeList Genre", "mal_producer": "MyAnimeList Producer"
"mal_favorite": "MyAnimeList Favorite", "mal_genre": "MyAnimeList Genre", "mal_studio": "MyAnimeList Studio"
}
season_sort_translation = {"score": "anime_score", "anime_score": "anime_score", "members": "anime_num_list_users", "anime_num_list_users": "anime_num_list_users"}
season_sort_options = ["score", "members"]
@ -191,15 +191,15 @@ class MyAnimeList:
util.print_end()
return mal_ids
def _producer(self, producer_id, limit):
data = self._jiken_request(f"/producer/{producer_id}")
def _studio(self, studio_id, limit):
data = self._jiken_request(f"/producer/{studio_id}")
if "anime" not in data:
raise Failed(f"MyAnimeList Error: No MyAnimeList IDs for Producer ID: {producer_id}")
raise Failed(f"MyAnimeList Error: No MyAnimeList IDs for Studio ID: {studio_id}")
mal_ids = []
count = 1
while True:
if count > 1:
data = self._jiken_request(f"/producer/{producer_id}/{count}")
data = self._jiken_request(f"/producer/{studio_id}/{count}")
if "anime" not in data:
break
mal_ids.extend([anime["mal_id"] for anime in data["anime"]])
@ -218,9 +218,9 @@ class MyAnimeList:
elif method == "mal_genre":
logger.info(f"Processing {mal_ranked_pretty[method]} ID: {data['genre_id']}")
mal_ids = self._genre(data["genre_id"], data["limit"])
elif method == "mal_producer":
logger.info(f"Processing {mal_ranked_pretty[method]} ID: {data['producer_id']}")
mal_ids = self._producer(data["producer_id"], data["limit"])
elif method == "mal_studio":
logger.info(f"Processing {mal_ranked_pretty[method]} ID: {data['studio_id']}")
mal_ids = self._studio(data["studio_id"], data["limit"])
elif method == "mal_season":
logger.info(f"Processing MyAnimeList Season: {data['limit']} Anime from {data['season'].title()} {data['year']} sorted by {pretty_names[data['sort_by']]}")
mal_ids = self._season(data["season"], data["year"], data["sort_by"], data["limit"])

@ -16,28 +16,54 @@ logger = logging.getLogger("Plex Meta Manager")
builders = ["plex_all", "plex_collectionless", "plex_search"]
search_translation = {
"audio_language": "audioLanguage",
"content_rating": "contentRating",
"subtitle_language": "subtitleLanguage",
"added": "addedAt",
"release": "originallyAvailableAt",
"audience_rating": "audienceRating",
"episode_title": "episode.title",
"network": "show.network",
"critic_rating": "rating",
"audience_rating": "audienceRating",
"user_rating": "userRating",
"plays": "viewCount",
"unplayed": "unwatched",
"episode_title": "episode.title",
"episode_user_rating": "episode.userRating",
"content_rating": "contentRating",
"episode_year": "episode.year",
"release": "originallyAvailableAt",
"episode_unmatched": "episode.unmatched",
"episode_duplicate": "episode.duplicate",
"added": "addedAt",
"episode_added": "episode.addedAt",
"episode_air_date": "episode.originallyAvailableAt",
"episode_year": "episode.year",
"episode_user_rating": "episode.userRating",
"episode_plays": "episode.viewCount"
"plays": "viewCount",
"episode_plays": "episode.viewCount",
"last_played": "lastViewedAt",
"episode_last_played": "episode.lastViewedAt",
"unplayed": "unwatched",
"episode_unplayed": "episode.unwatched",
"subtitle_language": "subtitleLanguage",
"audio_language": "audioLanguage",
"progress": "inProgress",
"episode_progress": "episode.inProgress",
"unplayed_episodes": "show.unwatchedLeaves"
}
show_translation = {
"title": "show.title",
"studio": "show.studio",
"rating": "show.rating",
"audienceRating": "show.audienceRating",
"userRating": "show.userRating",
"contentRating": "show.contentRating",
"year": "show.year",
"originallyAvailableAt": "show.originallyAvailableAt",
"unmatched": "show.unmatched",
"genre": "show.genre",
"collection": "show.collection",
"actor": "show.actor",
"addedAt": "show.addedAt",
"viewCount": "show.viewCount",
"lastViewedAt": "show.lastViewedAt",
"resolution": "episode.resolution",
"hdr": "episode.hdr",
"audioLanguage": "episode.audioLanguage",
"subtitleLanguage": "episode.subtitleLanguage",
"resolution": "episode.resolution"
"audioLanguage": "episode.audioLanguage",
"trash": "episode.trash",
"label": "show.label",
}
modifier_translation = {
"": "", ".not": "!", ".gt": "%3E%3E", ".gte": "%3E", ".lt": "%3C%3C", ".lte": "%3C",
@ -61,6 +87,7 @@ collection_mode_options = {
"show_items": "showItems", "showitems": "showItems"
}
collection_order_options = ["release", "alpha", "custom"]
collection_level_options = ["episode", "season"]
collection_mode_keys = {-1: "default", 0: "hide", 1: "hideItems", 2: "showItems"}
collection_order_keys = {0: "release", 1: "alpha", 2: "custom"}
item_advance_keys = {
@ -119,8 +146,8 @@ or_searches = [
]
movie_only_searches = [
"country", "country.not", "director", "director.not", "producer", "producer.not", "writer", "writer.not",
"decade", "duplicate", "unplayed", "progress", "trash",
"plays.gt", "plays.gte", "plays.lt", "plays.lte", "duration.gt", "duration.gte", "duration.lt", "duration.lte"
"decade", "duplicate", "unplayed", "progress",
"duration.gt", "duration.gte", "duration.lt", "duration.lte"
]
show_only_searches = [
"network", "network.not",
@ -128,9 +155,11 @@ show_only_searches = [
"episode_added", "episode_added.not", "episode_added.before", "episode_added.after",
"episode_air_date", "episode_air_date.not",
"episode_air_date.before", "episode_air_date.after",
"episode_last_played", "episode_last_played.not", "episode_last_played.before", "episode_last_played.after",
"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"
"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"]
boolean_attributes = [
@ -226,12 +255,17 @@ class Plex:
self.Plex = next((s for s in self.PlexServer.library.sections() if s.title == params["name"]), None)
if not self.Plex:
raise Failed(f"Plex Error: Plex Library {params['name']} not found")
if self.Plex.type not in ["movie", "show"]:
if self.Plex.type in ["movie", "show"]:
self.type = self.Plex.type.capitalize()
else:
raise Failed(f"Plex Error: Plex Library must be a Movies or TV Shows library")
self.agent = self.Plex.agent
self.is_movie = self.Plex.type == "movie"
self.is_show = self.Plex.type == "show"
self.is_movie = self.type == "Movie"
self.is_show = self.type == "Show"
self.is_other = self.agent == "com.plexapp.agents.none"
if self.is_other:
self.type = "Video"
self.collections = []
self.metadatas = []
@ -258,7 +292,7 @@ class Plex:
self.metadatas.extend([c for c in meta_obj.metadata])
self.metadata_files.append(meta_obj)
except Failed as e:
logger.error(e)
util.print_multiline(e, error=True)
if len(self.metadata_files) == 0:
logger.info("")
@ -336,7 +370,7 @@ class Plex:
return self.PlexServer.fetchItem(data)
def get_all(self):
logger.info(f"Loading All {'Movie' if self.is_movie else 'Show'}s from Library: {self.name}")
logger.info(f"Loading All {self.type}s from Library: {self.name}")
key = f"/library/sections/{self.Plex.key}/all?type={utils.searchType(self.Plex.TYPE)}"
container_start = 0
container_size = plexapi.X_PLEX_CONTAINER_SIZE
@ -345,7 +379,7 @@ class Plex:
results.extend(self.fetchItems(key, container_start, container_size))
util.print_return(f"Loaded: {container_start}/{self.Plex._totalViewSize}")
container_start += container_size
logger.info(util.adjust_space(f"Loaded {self.Plex._totalViewSize} {'Movies' if self.is_movie else 'Shows'}"))
logger.info(util.adjust_space(f"Loaded {self.Plex._totalViewSize} {self.type}s"))
return results
@retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex)
@ -395,10 +429,6 @@ class Plex:
@retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex)
def _upload_image(self, item, image):
logger.debug(item)
logger.debug(image.is_poster)
logger.debug(image.is_url)
logger.debug(image.location)
if image.is_poster and image.is_url:
item.uploadPoster(url=image.location)
elif image.is_poster:
@ -411,8 +441,6 @@ class Plex:
@retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex)
def upload_file_poster(self, item, image):
logger.debug(item)
logger.debug(image)
item.uploadPoster(filepath=image)
self.reload(item)
@ -439,6 +467,7 @@ class Plex:
if overlay is not None:
overlay_name, overlay_folder, overlay_image, temp_image = overlay
self.reload(item)
item_labels = {item_tag.tag.lower(): item_tag.tag for item_tag in item.labels}
for item_label in item_labels:
if item_label.endswith(" overlay") and item_label != f"{overlay_name.lower()} overlay":
@ -455,14 +484,18 @@ class Plex:
shutil.copyfile(temp_image, os.path.join(overlay_folder, f"{item.ratingKey}.png"))
while util.is_locked(temp_image):
time.sleep(1)
new_poster = Image.open(temp_image).convert("RGBA")
new_poster = new_poster.resize(overlay_image.size, Image.ANTIALIAS)
new_poster.paste(overlay_image, (0, 0), overlay_image)
new_poster.save(temp_image)
self.upload_file_poster(item, temp_image)
self.edit_tags("label", item, add_tags=[f"{overlay_name} Overlay"])
poster_uploaded = True
logger.info(f"Detail: Overlay: {overlay_name} applied to {item.title}")
try:
new_poster = Image.open(temp_image).convert("RGBA")
new_poster = new_poster.resize(overlay_image.size, Image.ANTIALIAS)
new_poster.paste(overlay_image, (0, 0), overlay_image)
new_poster.save(temp_image)
self.upload_file_poster(item, temp_image)
self.edit_tags("label", item, add_tags=[f"{overlay_name} Overlay"])
poster_uploaded = True
logger.info(f"Detail: Overlay: {overlay_name} applied to {item.title}")
except OSError as e:
util.print_stacktrace()
logger.error(f"Overlay Error: {e}")
background_uploaded = False
if background is not None:
@ -601,10 +634,9 @@ class Plex:
return valid_collections
def get_rating_keys(self, method, data):
media_type = "Movie" if self.is_movie else "Show"
items = []
if method == "plex_all":
logger.info(f"Processing Plex All {media_type}s")
logger.info(f"Processing Plex All {self.type}s")
items = self.get_all()
elif method == "plex_search":
util.print_multiline(data[1], info=True)
@ -645,7 +677,7 @@ class Plex:
break
if add_item:
items.append(item)
logger.info(util.adjust_space(f"Processed {len(all_items)} {'Movies' if self.is_movie else 'Shows'}"))
logger.info(util.adjust_space(f"Processed {len(all_items)} {self.type}s"))
else:
raise Failed(f"Plex Error: Method {method} not supported")
if len(items) > 0:
@ -669,7 +701,7 @@ class Plex:
try:
yaml.round_trip_dump(self.missing, open(self.missing_path, "w"))
except yaml.scanner.ScannerError as e:
logger.error(f"YAML Error: {util.tab_new_lines(e)}")
util.print_multiline(f"YAML Error: {util.tab_new_lines(e)}", error=True)
def get_collection_items(self, collection, smart_label_collection):
if smart_label_collection:
@ -692,7 +724,7 @@ class Plex:
def map_guids(self):
items = self.get_all()
logger.info(f"Mapping {'Movie' if self.is_movie else 'Show'} Library: {self.name}")
logger.info(f"Mapping {self.type} Library: {self.name}")
logger.info("")
for i, item in enumerate(items, 1):
util.print_return(f"Processing: {i}/{len(items)} {item.title}")
@ -708,7 +740,7 @@ class Plex:
if imdb_id:
util.add_dict_list(imdb_id, item.ratingKey, self.imdb_map)
logger.info("")
logger.info(util.adjust_space(f"Processed {len(items)} {'Movies' if self.is_movie else 'Shows'}"))
logger.info(util.adjust_space(f"Processed {len(items)} {self.type}s"))
return items
def get_tmdb_from_map(self, item):
@ -741,27 +773,28 @@ class Plex:
return False
def edit_tags(self, attr, obj, add_tags=None, remove_tags=None, sync_tags=None):
updated = False
display = ""
key = builder.filter_translation[attr] if attr in builder.filter_translation else attr
if add_tags or remove_tags or sync_tags:
_add_tags = add_tags if add_tags else []
_remove_tags = [t.lower() for t in remove_tags] if remove_tags else []
_sync_tags = [t.lower() for t in sync_tags] if sync_tags else []
try:
self.reload(obj)
_item_tags = [item_tag.tag.lower() for item_tag in getattr(obj, key)]
except BadRequest:
_item_tags = []
_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 and t not in _sync_tags) or t in _remove_tags]
if _add:
updated = True
self.query_data(getattr(obj, f"add{attr.capitalize()}"), _add)
logger.info(f"Detail: {attr.capitalize()} {','.join(_add)} added to {obj.title}")
display += f"+{', +'.join(_add)}"
if _remove:
updated = True
self.query_data(getattr(obj, f"remove{attr.capitalize()}"), _remove)
logger.info(f"Detail: {attr.capitalize()} {','.join(_remove)} removed to {obj.title}")
return updated
display += f"-{', -'.join(_remove)}"
if len(display) > 0:
logger.info(f"{obj.title[:25]:<25} | {attr.capitalize()} | {display}")
return len(display) > 0
def update_item_from_assets(self, item, overlay=None, create=False):
name = os.path.basename(os.path.dirname(str(item.locations[0])) if self.is_movie else str(item.locations[0]))

@ -16,6 +16,7 @@ sorts = [
"rank", "added", "title", "released", "runtime", "popularity",
"percentage", "votes", "random", "my_rating", "watched", "collected"
]
id_translation = {"movie": "tmdb", "show": "tvdb", "season": "TVDb Season", "episode": "TVDb Episode"}
class Trakt:
def __init__(self, config, params):
@ -142,26 +143,31 @@ class Trakt:
except Failed:
raise Failed(f"Trakt Error: List {data} not found")
def _parse(self, items, top=True, item_type=None):
def _parse(self, items, typeless=False, item_type=None):
ids = []
for item in items:
if top:
if item_type:
data = item[item_type]
elif item["type"] in ["movie", "show"]:
data = item[item["type"]]
else:
continue
else:
if typeless:
data = item
if item_type:
id_type = "TMDb" if item_type == "movie" else "TVDb"
current_type = None
elif item_type:
data = item[item_type]
current_type = item_type
elif "type" in item and item["type"] in id_translation:
data = item["movie" if item["type"] == "movie" else "show"]
current_type = item["type"]
else:
id_type = "TMDb" if item["type"] == "movie" else "TVDb"
if data["ids"][id_type.lower()]:
ids.append((data["ids"][id_type.lower()], id_type.lower()))
continue
id_type = "tmdb" if current_type == "movie" else "tvdb"
if data["ids"][id_type]:
final_id = data["ids"][id_type]
if current_type == "episode":
final_id = f"{final_id}_{item[current_type]['season']}"
if current_type in ["episode", "season"]:
final_id = f"{final_id}_{item[current_type]['number']}"
final_type = f"{id_type}_{current_type}" if current_type in ["episode", "season"] else id_type
ids.append((final_id, final_type))
else:
logger.error(f"Trakt Error: No {id_type} ID found for {data['title']} ({data['year']})")
logger.error(f"Trakt Error: No {id_type.upper().replace('B', 'b')} ID found for {data['title']} ({data['year']})")
return ids
def _user_list(self, data):
@ -184,7 +190,7 @@ class Trakt:
def _pagenation(self, pagenation, amount, is_movie):
items = self._request(f"/{'movies' if is_movie else 'shows'}/{pagenation}?limit={amount}")
return self._parse(items, top=pagenation != "popular", item_type="movie" if is_movie else "show")
return self._parse(items, typeless=pagenation == "popular", item_type="movie" if is_movie else "show")
def validate_trakt(self, trakt_lists, is_movie, trakt_type="list"):
values = util.get_list(trakt_lists, split=False)

@ -66,6 +66,9 @@ def tab_new_lines(data):
def make_ordinal(n):
return f"{n}{'th' if 11 <= (n % 100) <= 13 else ['th', 'st', 'nd', 'rd', 'th'][min(n % 10, 4)]}"
def add_zero(number):
return str(number) if len(str(number)) > 1 else f"0{number}"
def add_dict_list(keys, value, dict_map):
for key in keys:
if key in dict_map:

@ -108,7 +108,7 @@ def start(config_path, is_test=False, time_scheduled=None, requested_collections
logger.info(util.centered("| __/| | __/> < | | | | __/ || (_| | | | | | (_| | | | | (_| | (_| | __/ | "))
logger.info(util.centered("|_| |_|\\___/_/\\_\\ |_| |_|\\___|\\__\\__,_| |_| |_|\\__,_|_| |_|\\__,_|\\__, |\\___|_| "))
logger.info(util.centered(" |___/ "))
logger.info(util.centered(" Version: 1.12.0 "))
logger.info(util.centered(" Version: 1.12.1 "))
if time_scheduled: start_type = f"{time_scheduled} "
elif is_test: start_type = "Test "
elif requested_collections: start_type = "Collections "
@ -125,7 +125,7 @@ def start(config_path, is_test=False, time_scheduled=None, requested_collections
update_libraries(config)
except Exception as e:
util.print_stacktrace()
logger.critical(e)
util.print_multiline(e, critical=True)
logger.info("")
util.separator(f"Finished {start_type}Run\nRun Time: {str(datetime.now() - start_time).split('.')[0]}")
logger.removeHandler(file_handler)
@ -144,12 +144,14 @@ def update_libraries(config):
os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(library.timeout)
logger.info("")
util.separator(f"{library.name} Library")
logger.info("")
util.separator(f"Mapping {library.name} Library", space=False, border=False)
logger.info("")
items = library.map_guids()
items = None
if not library.is_other:
logger.info("")
util.separator(f"Mapping {library.name} Library", space=False, border=False)
logger.info("")
items = library.map_guids()
if not config.test_mode and not config.resume_from and not collection_only and library.mass_update:
mass_metadata(config, library, items)
mass_metadata(config, library, items=items)
for metadata in library.metadata_files:
logger.info("")
util.separator(f"Running Metadata File\n{metadata.path}")
@ -198,7 +200,7 @@ def update_libraries(config):
if library.assets_for_all and not collection_only:
logger.info("")
util.separator(f"All {'Movies' if library.is_movie else 'Shows'} Assets Check for {library.name} Library", space=False, border=False)
util.separator(f"All {library.type}s Assets Check for {library.name} Library", space=False, border=False)
logger.info("")
for col in unmanaged_collections:
poster, background = library.find_collection_assets(col, create=library.create_asset_folders)
@ -257,10 +259,12 @@ def update_libraries(config):
if library.optimize:
library.query(library.PlexServer.library.optimize)
def mass_metadata(config, library, items):
def mass_metadata(config, library, items=None):
logger.info("")
util.separator(f"Mass Editing {'Movie' if library.is_movie else 'Show'} Library: {library.name}")
util.separator(f"Mass Editing {library.type} Library: {library.name}")
logger.info("")
if items is None:
items = library.get_all()
if library.split_duplicates:
items = library.search(**{"duplicate": True})
for item in items:
@ -366,18 +370,7 @@ def mass_metadata(config, library, items):
new_genres = tvdb_item.genres
else:
raise Failed
item_genres = [genre.tag for genre in item.genres]
display_str = ""
add_genre = [genre for genre in (g for g in new_genres if g not in item_genres)]
if len(add_genre) > 0:
display_str += f"+{', +'.join(add_genre)}"
library.query_data(item.addGenre, add_genre)
remove_genre = [genre for genre in (g for g in item_genres if g not in new_genres)]
if len(remove_genre) > 0:
display_str += f"-{', -'.join(remove_genre)}"
library.query_data(item.removeGenre, remove_genre)
if len(display_str) > 0:
logger.info(util.adjust_space(f"{item.title[:25]:<25} | Genres | {display_str}"))
library.edit_tags("genre", item, sync_tags=new_genres)
except Failed:
pass
if library.mass_audience_rating_update:

Loading…
Cancel
Save