#365 added collection_level

This commit is contained in:
meisnate12 2021-08-22 11:54:33 -04:00
parent ddc81cde11
commit ec0b1ba329
4 changed files with 125 additions and 49 deletions

View file

@ -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")
@ -64,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",
@ -78,13 +78,19 @@ poster_details = ["url_poster", "tmdb_poster", "tmdb_profile", "tvdb_poster", "f
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"]
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",
@ -130,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",
@ -142,6 +148,10 @@ custom_sort_builders = [
"mal_all", "mal_airing", "mal_upcoming", "mal_tv", "mal_movie", "mal_ova", "mal_special",
"mal_popular", "mal_favorite", "mal_suggested", "mal_userlist", "mal_season", "mal_genre", "mal_studio"
]
parts_collection_valid = [
"plex_search", "trakt_list", "trakt_list_details", "collection_mode", "label", "visible_library",
"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):
@ -165,6 +175,7 @@ class CollectionBuilder:
self.sonarr_details = {}
self.missing_movies = []
self.missing_shows = []
self.missing_parts = []
self.builders = []
self.filters = []
self.tmdb_filters = []
@ -411,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")
@ -479,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
@ -501,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")
@ -869,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)
@ -1070,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)
@ -1086,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:
@ -1103,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:
@ -1125,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:
@ -1133,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}")
@ -1149,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"]]
@ -1388,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):
@ -1404,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:
@ -1610,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
@ -1620,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:
@ -1628,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
@ -1697,7 +1769,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:
@ -1890,7 +1962,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
@ -1912,13 +1985,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("")

View file

@ -61,6 +61,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 = {
@ -395,10 +396,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 +408,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)

View file

@ -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:
if typeless:
data = item
current_type = None
elif item_type:
data = item[item_type]
elif item["type"] in ["movie", "show"]:
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:
continue
id_type = "tmdb" if item["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:
data = item
if item_type:
id_type = "TMDb" if item_type == "movie" else "TVDb"
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()))
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)

View file

@ -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: