mirror of
https://github.com/meisnate12/Plex-Meta-Manager
synced 2024-11-10 15:04:21 +00:00
commit
e6fd8bf92c
8 changed files with 254 additions and 140 deletions
|
@ -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"])
|
||||
|
|
133
modules/plex.py
133
modules/plex.py
|
@ -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",
|
||||
"critic_rating": "rating",
|
||||
"user_rating": "userRating",
|
||||
"plays": "viewCount",
|
||||
"unplayed": "unwatched",
|
||||
"episode_title": "episode.title",
|
||||
"network": "show.network",
|
||||
"critic_rating": "rating",
|
||||
"audience_rating": "audienceRating",
|
||||
"user_rating": "userRating",
|
||||
"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…
Reference in a new issue