From 86e07232e39905d579b3403ff5d253b6f3b588ac Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Sat, 12 Mar 2022 09:04:41 -0500 Subject: [PATCH 01/32] [1] fix mass_genre options --- VERSION | 2 +- modules/config.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/VERSION b/VERSION index 41c11ffb7..6ba927a36 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1 +1.16.1-develop1 diff --git a/modules/config.py b/modules/config.py index 5cb4e5f4a..6f2ad3daa 100644 --- a/modules/config.py +++ b/modules/config.py @@ -31,7 +31,7 @@ logger = util.logger sync_modes = {"append": "Only Add Items to the Collection or Playlist", "sync": "Add & Remove Items from the Collection or Playlist"} -mass_update_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb"} +mass_genre_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb", "tvdb": "Use TVDb Metadata"} mass_content_options = {"omdb": "Use IMDb Metadata through OMDb", "mdb": "Use MdbList Metadata", "mdb_commonsense": "Use Commonsense Rating through MDbList"} mass_rating_options = { "tmdb": "Use TMDb Rating", @@ -642,7 +642,7 @@ def check_dict(attr): params["error_webhooks"] = check_for_attribute(lib, "error", parent="webhooks", var_type="list", default=self.webhooks["error"], do_print=False, save=False, default_is_none=True) params["changes_webhooks"] = check_for_attribute(lib, "changes", parent="webhooks", var_type="list", default=self.webhooks["changes"], do_print=False, save=False, default_is_none=True) params["assets_for_all"] = check_for_attribute(lib, "assets_for_all", parent="settings", var_type="bool", default=self.general["assets_for_all"], do_print=False, save=False) - params["mass_genre_update"] = check_for_attribute(lib, "mass_genre_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=False) + params["mass_genre_update"] = check_for_attribute(lib, "mass_genre_update", test_list=mass_genre_options, default_is_none=True, save=False, do_print=False) params["mass_audience_rating_update"] = check_for_attribute(lib, "mass_audience_rating_update", test_list=mass_rating_options, default_is_none=True, save=False, do_print=False) params["mass_critic_rating_update"] = check_for_attribute(lib, "mass_critic_rating_update", test_list=mass_rating_options, default_is_none=True, save=False, do_print=False) params["mass_trakt_rating_update"] = check_for_attribute(lib, "mass_trakt_rating_update", var_type="bool", default=False, save=False, do_print=False) @@ -660,7 +660,7 @@ def check_dict(attr): if "delete_collections_with_less" in lib["operations"]: params["delete_collections_with_less"] = check_for_attribute(lib["operations"], "delete_collections_with_less", var_type="int", default_is_none=True, save=False) if "mass_genre_update" in lib["operations"]: - params["mass_genre_update"] = check_for_attribute(lib["operations"], "mass_genre_update", test_list=mass_update_options, default_is_none=True, save=False) + params["mass_genre_update"] = check_for_attribute(lib["operations"], "mass_genre_update", test_list=mass_genre_options, default_is_none=True, save=False) if "mass_audience_rating_update" in lib["operations"]: params["mass_audience_rating_update"] = check_for_attribute(lib["operations"], "mass_audience_rating_update", test_list=mass_rating_options, default_is_none=True, save=False) if "mass_critic_rating_update" in lib["operations"]: From dec20321bdd74162d22c3d0fe287bcafddc199c8 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Sat, 12 Mar 2022 10:18:44 -0500 Subject: [PATCH 02/32] [2] fix decade dynamic collections --- VERSION | 2 +- modules/meta.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/VERSION b/VERSION index 6ba927a36..fd6ec9f5c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop1 +1.16.1-develop2 diff --git a/modules/meta.py b/modules/meta.py index 045c2a2d0..9d44b9cfd 100644 --- a/modules/meta.py +++ b/modules/meta.py @@ -268,11 +268,11 @@ def __init__(self, config, library, file_type, path): if og_exclude and include: raise Failed(f"Config Error: {map_name} cannot have both include and exclude attributes") addons = util.parse("Config", "addons", dynamic, parent=map_name, methods=methods, datatype="dictlist") if "addons" in methods else {} - exclude = [e for e in og_exclude] + exclude = [str(e) for e in og_exclude] for k, v in addons.items(): if k in v: logger.warning(f"Config Warning: {k} cannot be an addon for itself") - exclude.extend([vv for vv in v if vv != k]) + exclude.extend([str(vv) for vv in v if str(vv) != str(k)]) default_title_format = "<>" default_template = None auto_list = {} @@ -283,7 +283,7 @@ def _check_dict(check_dict): auto_list[ck] = cv if auto_type in ["genre", "mood", "style", "country", "network", "year", "decade", "content_rating", "subtitle_language", "audio_language", "resolution"]: search_tag = auto_type_translation[auto_type] if auto_type in auto_type_translation else auto_type - if auto_type in ["subtitle_language", "audio_language"]: + if auto_type in ["decade", "subtitle_language", "audio_language"]: auto_list = {i.key: i.title for i in library.get_tags(search_tag) if i.title not in exclude and i.key not in exclude} else: auto_list = {i.title: i.title for i in library.get_tags(search_tag) if i.title not in exclude} From 14d061ac62e74df1abf5637940fed28ae697838e Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Sat, 12 Mar 2022 10:34:43 -0500 Subject: [PATCH 03/32] [3] make a dynamic keys strings --- VERSION | 2 +- modules/meta.py | 12 ++++++------ modules/tmdb.py | 2 +- modules/trakt.py | 2 +- modules/util.py | 4 +++- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/VERSION b/VERSION index fd6ec9f5c..30d365655 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop2 +1.16.1-develop3 diff --git a/modules/meta.py b/modules/meta.py index 9d44b9cfd..b5d0c2739 100644 --- a/modules/meta.py +++ b/modules/meta.py @@ -284,9 +284,9 @@ def _check_dict(check_dict): if auto_type in ["genre", "mood", "style", "country", "network", "year", "decade", "content_rating", "subtitle_language", "audio_language", "resolution"]: search_tag = auto_type_translation[auto_type] if auto_type in auto_type_translation else auto_type if auto_type in ["decade", "subtitle_language", "audio_language"]: - auto_list = {i.key: i.title for i in library.get_tags(search_tag) if i.title not in exclude and i.key not in exclude} + auto_list = {str(i.key): i.title for i in library.get_tags(search_tag) if str(i.title) not in exclude and str(i.key) not in exclude} else: - auto_list = {i.title: i.title for i in library.get_tags(search_tag) if i.title not in exclude} + auto_list = {str(i.title): i.title for i in library.get_tags(search_tag) if str(i.title) not in exclude} if library.is_music: default_template = {"smart_filter": {"limit": 50, "sort_by": "plays.desc", "any": {f"artist_{auto_type}": f"<<{auto_type}>>"}}} default_title_format = "Most Played <> <>s" @@ -304,7 +304,7 @@ def _check_dict(check_dict): tmdb_id, tvdb_id, imdb_id = library.get_ids(item) tmdb_item = config.TMDb.get_item(item, tmdb_id, tvdb_id, imdb_id, is_movie=True) if tmdb_item and tmdb_item.collection and tmdb_item.collection.id not in exclude and tmdb_item.collection.name not in exclude: - auto_list[tmdb_item.collection.id] = tmdb_item.collection.name + auto_list[str(tmdb_item.collection.id)] = tmdb_item.collection.name logger.exorcise() elif auto_type == "original_language": if not all_items: @@ -369,7 +369,7 @@ def _check_dict(check_dict): try: results = self.config.TMDb.search_people(role["name"]) if results[0].id not in exclude: - auto_list[results[0].id] = results[0].name + auto_list[str(results[0].id)] = results[0].name person_count += 1 except TMDbNotFound: logger.error(f"TMDb Error: Actor {role['name']} Not Found") @@ -399,8 +399,8 @@ def _check_dict(check_dict): methods["title_override"] = methods.pop("post_format_override") if "pre_format_override" in methods: methods["key_name_override"] = methods.pop("pre_format_override") - title_override = util.parse("Config", "title_override", dynamic, parent=map_name, methods=methods, datatype="dict") if "title_override" in methods else {} - key_name_override = util.parse("Config", "key_name_override", dynamic, parent=map_name, methods=methods, datatype="dict") if "key_name_override" in methods else {} + title_override = util.parse("Config", "title_override", dynamic, parent=map_name, methods=methods, datatype="strdict") if "title_override" in methods else {} + key_name_override = util.parse("Config", "key_name_override", dynamic, parent=map_name, methods=methods, datatype="strdict") if "key_name_override" in methods else {} test = util.parse("Config", "test", dynamic, parent=map_name, methods=methods, default=False, datatype="bool") if "test" in methods else False sync = util.parse("Config", "sync", dynamic, parent=map_name, methods=methods, default=False, datatype="bool") if "sync" in methods else False if "<>" in title_format: diff --git a/modules/tmdb.py b/modules/tmdb.py index d47ac9ff8..34e99e433 100644 --- a/modules/tmdb.py +++ b/modules/tmdb.py @@ -139,7 +139,7 @@ def get_list(self, tmdb_id): except TMDbException as e: raise Failed(f"TMDb Error: No List found for TMDb ID {tmdb_id}: {e}") def get_popular_people(self, limit): - return {p.id: p.name for p in self.TMDb.popular_people().get_results(limit)} + return {str(p.id): p.name for p in self.TMDb.popular_people().get_results(limit)} def search_people(self, name): return self.TMDb.people_search(name) diff --git a/modules/trakt.py b/modules/trakt.py index d290013fe..51ecd59cd 100644 --- a/modules/trakt.py +++ b/modules/trakt.py @@ -241,7 +241,7 @@ def _pagenation(self, pagenation, amount, is_movie): return self._parse(items, typeless=pagenation == "popular", item_type="movie" if is_movie else "show") def get_people(self, data): - return {i[0][0]: i[0][1] for i in self._user_list(data) if i[1] == "tmdb_person"} + return {str(i[0][0]): i[0][1] for i in self._user_list(data) if i[1] == "tmdb_person"} def validate_trakt(self, trakt_lists, is_movie, trakt_type="list"): values = util.get_list(trakt_lists, split=False) diff --git a/modules/util.py b/modules/util.py index f872588ee..85e2a83f6 100644 --- a/modules/util.py +++ b/modules/util.py @@ -438,12 +438,14 @@ def parse(error, attribute, data, datatype=None, methods=None, parent=None, defa else: raise Failed(f"{error} Error: {display} {dict_data} is not a dictionary") return final_list - elif datatype in ["dict", "dictlist", "dictdict"]: + elif datatype in ["dict", "dictlist", "dictdict", "strdict"]: if isinstance(value, dict): if datatype == "dict": return value elif datatype == "dictlist": return {k: v if isinstance(v, list) else [v] for k, v in value.items()} + elif datatype == "strdict": + return {str(k): str(v) for k, v in value.items()} else: final_dict = {} for dict_key, dict_data in value.items(): From 6b3c75926ac4a0018b38d5c6d9d818b3f8c2db1b Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Sat, 12 Mar 2022 16:59:37 -0500 Subject: [PATCH 04/32] [4] #769 add plex_pilots to playlists --- VERSION | 2 +- modules/builder.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 30d365655..11e9c31f7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop3 +1.16.1-develop4 diff --git a/modules/builder.py b/modules/builder.py index 50487fcf0..c44861671 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -169,7 +169,7 @@ smart_invalid = ["collection_order", "collection_level"] smart_url_invalid = ["minimum_items", "filters", "run_again", "sync_mode", "show_filtered", "show_missing", "save_missing", "smart_label"] + radarr_details + sonarr_details custom_sort_builders = [ - "plex_search", "tmdb_list", "tmdb_popular", "tmdb_now_playing", "tmdb_top_rated", + "plex_search", "plex_pilots", "tmdb_list", "tmdb_popular", "tmdb_now_playing", "tmdb_top_rated", "tmdb_trending_daily", "tmdb_trending_weekly", "tmdb_discover", "tvdb_list", "imdb_chart", "imdb_list", "stevenlu_popular", "anidb_popular", "trakt_list", "trakt_watchlist", "trakt_collection", "trakt_trending", "trakt_popular", "trakt_boxoffice", From 3a7f56176fa27ba31dd09bd63fc0892f161dd259 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Sun, 13 Mar 2022 15:39:34 -0400 Subject: [PATCH 05/32] [5] cache tmdb movies/shows --- VERSION | 2 +- modules/builder.py | 12 ++-- modules/cache.py | 150 +++++++++++++++++++++++++++++++++++++++++++ modules/config.py | 3 +- modules/meta.py | 24 +++---- modules/tmdb.py | 101 +++++++++++++++++++++++++++-- plex_meta_manager.py | 6 +- 7 files changed, 267 insertions(+), 31 deletions(-) diff --git a/VERSION b/VERSION index 11e9c31f7..bc2cbf693 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop4 +1.16.1-develop5 diff --git a/modules/builder.py b/modules/builder.py index c44861671..bdcab5da7 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -1898,9 +1898,9 @@ def check_tmdb_filter(self, item_id, is_movie, item=None, check_released=False): try: if item is None: if is_movie: - item = self.config.TMDb.get_movie(item_id, partial="keywords") + item = self.config.TMDb.get_movie(item_id) else: - item = self.config.TMDb.get_show(self.config.Convert.tvdb_to_tmdb(item_id), partial="keywords") + item = self.config.TMDb.get_show(self.config.Convert.tvdb_to_tmdb(item_id)) if check_released: date_to_check = item.release_date if is_movie else item.first_air_date if not date_to_check or date_to_check > self.current_time: @@ -1913,7 +1913,7 @@ def check_tmdb_filter(self, item_id, is_movie, item=None, check_released=False): elif filter_attr == "tmdb_type": check_value = discover_types[item.type] elif filter_attr == "original_language": - check_value = item.original_language.iso_639_1 + check_value = item.language_iso else: raise Failed if (modifier == ".not" and check_value in filter_data) or (modifier == "" and check_value not in filter_data): @@ -1936,11 +1936,11 @@ def check_tmdb_filter(self, item_id, is_movie, item=None, check_released=False): return False elif filter_attr in ["tmdb_genre", "tmdb_keyword", "origin_country"]: if filter_attr == "tmdb_genre": - attrs = [g.name for g in item.genres] + attrs = item.genres elif filter_attr == "tmdb_keyword": - attrs = [k.name for k in item.keywords] + attrs = item.keywords elif filter_attr == "origin_country": - attrs = [c.iso_3166_1 for c in item.origin_countries] + attrs = [c.iso_3166_1 for c in item.countries] else: raise Failed if (not list(set(filter_data) & set(attrs)) and modifier == "") \ diff --git a/modules/cache.py b/modules/cache.py index f16d93874..6666e828d 100644 --- a/modules/cache.py +++ b/modules/cache.py @@ -109,6 +109,56 @@ def __init__(self, config_path, expiration): certification TEXT, expiration_date TEXT)""" ) + cursor.execute( + """CREATE TABLE IF NOT EXISTS tmdb_movie_data ( + key INTEGER PRIMARY KEY, + tmdb_id INTEGER UNIQUE, + title TEXT, + original_title TEXT, + studio TEXT, + overview TEXT, + tagline TEXT, + imdb_id TEXT, + poster_url TEXT, + backdrop_url TEXT, + vote_count INTEGER, + vote_average REAL, + language_iso TEXT, + language_name TEXT, + genres TEXT, + keywords TEXT, + release_date TEXT, + collection_id INTEGER, + collection_name TEXT, + expiration_date TEXT)""" + ) + cursor.execute( + """CREATE TABLE IF NOT EXISTS tmdb_show_data ( + key INTEGER PRIMARY KEY, + tmdb_id INTEGER UNIQUE, + title TEXT, + original_title TEXT, + studio TEXT, + overview TEXT, + tagline TEXT, + imdb_id TEXT, + poster_url TEXT, + backdrop_url TEXT, + vote_count INTEGER, + vote_average REAL, + language_iso TEXT, + language_name TEXT, + genres TEXT, + keywords TEXT, + first_air_date TEXT, + last_air_date TEXT, + status TEXT, + type TEXT, + tvdb_id INTEGER, + countries TEXT, + seasons TEXT, + expiration_date TEXT)""" + ) cursor.execute( """CREATE TABLE IF NOT EXISTS anime_map ( key INTEGER PRIMARY KEY, @@ -361,6 +411,106 @@ def update_mdb(self, expired, key_id, mdb, expiration): mdb.commonsense, expiration_date.strftime("%Y-%m-%d"), key_id )) + def query_tmdb_movie(self, tmdb_id, expiration): + tmdb_dict = {} + expired = None + with sqlite3.connect(self.cache_path) as connection: + connection.row_factory = sqlite3.Row + with closing(connection.cursor()) as cursor: + cursor.execute("SELECT * FROM tmdb_movie_data WHERE tmdb_id = ?", (tmdb_id,)) + row = cursor.fetchone() + if row: + tmdb_dict["title"] = row["title"] if row["title"] else "" + tmdb_dict["original_title"] = row["original_title"] if row["original_title"] else "" + tmdb_dict["studio"] = row["studio"] if row["studio"] else "" + tmdb_dict["overview"] = row["overview"] if row["overview"] else "" + tmdb_dict["tagline"] = row["tagline"] if row["tagline"] else "" + tmdb_dict["imdb_id"] = row["imdb_id"] if row["imdb_id"] else "" + tmdb_dict["poster_url"] = row["poster_url"] if row["poster_url"] else "" + tmdb_dict["backdrop_url"] = row["backdrop_url"] if row["backdrop_url"] else "" + tmdb_dict["vote_count"] = row["vote_count"] if row["vote_count"] else 0 + tmdb_dict["vote_average"] = row["vote_average"] if row["vote_average"] else 0 + tmdb_dict["language_iso"] = row["language_iso"] if row["language_iso"] else None + tmdb_dict["language_name"] = row["language_name"] if row["language_name"] else None + tmdb_dict["genres"] = row["genres"] if row["genres"] else "" + tmdb_dict["keywords"] = row["keywords"] if row["keywords"] else "" + tmdb_dict["release_date"] = datetime.strptime(row["release_date"], "%Y-%m-%d") if row["release_date"] else None + tmdb_dict["collection_id"] = row["collection_id"] if row["collection_id"] else None + tmdb_dict["collection_name"] = row["collection_name"] if row["collection_name"] else None + datetime_object = datetime.strptime(row["expiration_date"], "%Y-%m-%d") + time_between_insertion = datetime.now() - datetime_object + expired = time_between_insertion.days > expiration + return tmdb_dict, expired + + def update_tmdb_movie(self, expired, obj, expiration): + expiration_date = datetime.now() if expired is True else (datetime.now() - timedelta(days=random.randint(1, expiration))) + with sqlite3.connect(self.cache_path) as connection: + connection.row_factory = sqlite3.Row + with closing(connection.cursor()) as cursor: + cursor.execute("INSERT OR IGNORE INTO tmdb_movie_data(tmdb_id) VALUES(?)", (obj.tmdb_id,)) + update_sql = "UPDATE tmdb_movie_data SET title = ?, original_title = ?, studio = ?, overview = ?, tagline = ?, imdb_id = ?, " \ + "poster_url = ?, backdrop_url = ?, vote_count = ?, vote_average = ?, language_iso = ?, " \ + "language_name = ?, genres = ?, keywords = ?, release_date = ?, collection_id = ?, " \ + "collection_name = ?, expiration_date = ? WHERE tmdb_id = ?" + cursor.execute(update_sql, ( + obj.title, obj.original_title, obj.studio, obj.overview, obj.tagline, obj.imdb_id, obj.poster_url, obj.backdrop_url, + obj.vote_count, obj.vote_average, obj.language_iso, obj.language_name, "|".join(obj.genres), "|".join(obj.keywords), + obj.release_date.strftime("%Y-%m-%d") if obj.release_date else None, obj.collection_id, obj.collection_name, + expiration_date.strftime("%Y-%m-%d"), obj.tmdb_id + )) + + def query_tmdb_show(self, tmdb_id, expiration): + tmdb_dict = {} + expired = None + with sqlite3.connect(self.cache_path) as connection: + connection.row_factory = sqlite3.Row + with closing(connection.cursor()) as cursor: + cursor.execute("SELECT * FROM tmdb_show_data WHERE tmdb_id = ?", (tmdb_id,)) + row = cursor.fetchone() + if row: + tmdb_dict["title"] = row["title"] if row["title"] else "" + tmdb_dict["original_title"] = row["original_title"] if row["original_title"] else "" + tmdb_dict["studio"] = row["studio"] if row["studio"] else "" + tmdb_dict["overview"] = row["overview"] if row["overview"] else "" + tmdb_dict["tagline"] = row["tagline"] if row["tagline"] else "" + tmdb_dict["imdb_id"] = row["imdb_id"] if row["imdb_id"] else "" + tmdb_dict["poster_url"] = row["poster_url"] if row["poster_url"] else "" + tmdb_dict["backdrop_url"] = row["backdrop_url"] if row["backdrop_url"] else "" + tmdb_dict["vote_count"] = row["vote_count"] if row["vote_count"] else 0 + tmdb_dict["vote_average"] = row["vote_average"] if row["vote_average"] else 0 + tmdb_dict["language_iso"] = row["language_iso"] if row["language_iso"] else None + tmdb_dict["language_name"] = row["language_name"] if row["language_name"] else None + tmdb_dict["genres"] = row["genres"] if row["genres"] else "" + tmdb_dict["keywords"] = row["keywords"] if row["keywords"] else "" + tmdb_dict["first_air_date"] = datetime.strptime(row["first_air_date"], "%Y-%m-%d") if row["first_air_date"] else None + tmdb_dict["last_air_date"] = datetime.strptime(row["last_air_date"], "%Y-%m-%d") if row["last_air_date"] else None + tmdb_dict["status"] = row["status"] if row["status"] else None + tmdb_dict["type"] = row["type"] if row["type"] else None + tmdb_dict["tvdb_id"] = row["tvdb_id"] if row["tvdb_id"] else None + datetime_object = datetime.strptime(row["expiration_date"], "%Y-%m-%d") + time_between_insertion = datetime.now() - datetime_object + expired = time_between_insertion.days > expiration + return tmdb_dict, expired + + def update_tmdb_show(self, expired, obj, expiration): + expiration_date = datetime.now() if expired is True else (datetime.now() - timedelta(days=random.randint(1, expiration))) + with sqlite3.connect(self.cache_path) as connection: + connection.row_factory = sqlite3.Row + with closing(connection.cursor()) as cursor: + cursor.execute("INSERT OR IGNORE INTO tmdb_show_data(tmdb_id) VALUES(?)", (obj.tmdb_id,)) + update_sql = "UPDATE tmdb_show_data SET title = ?, original_title = ?, studio = ?, overview = ?, tagline = ?, imdb_id = ?, " \ + "poster_url = ?, backdrop_url = ?, vote_count = ?, vote_average = ?, language_iso = ?, " \ + "language_name = ?, genres = ?, keywords = ?, first_air_date = ?, last_air_date = ?, status = ?, " \ + "type = ?, tvdb_id = ?, countries = ?, seasons = ?, expiration_date = ? WHERE tmdb_id = ?" + cursor.execute(update_sql, ( + obj.title, obj.original_title, obj.studio, obj.overview, obj.tagline, obj.imdb_id, obj.poster_url, obj.backdrop_url, + obj.vote_count, obj.vote_average, obj.language_iso, obj.language_name, "|".join(obj.genres), "|".join(obj.keywords), + obj.first_air_date.strftime("%Y-%m-%d") if obj.first_air_date else None, + obj.last_air_date.strftime("%Y-%m-%d") if obj.last_air_date else None, + obj.status, obj.type, obj.tvdb_id, "|".join(obj.countries), "|".join(obj.seasons), + expiration_date.strftime("%Y-%m-%d"), obj.tmdb_id + )) + def query_anime_map(self, anime_id, id_type): ids = None expired = None diff --git a/modules/config.py b/modules/config.py index 6f2ad3daa..566ed5fab 100644 --- a/modules/config.py +++ b/modules/config.py @@ -366,7 +366,8 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No logger.info("Connecting to TMDb...") self.TMDb = TMDb(self, { "apikey": check_for_attribute(self.data, "apikey", parent="tmdb", throw=True), - "language": check_for_attribute(self.data, "language", parent="tmdb", default="en") + "language": check_for_attribute(self.data, "language", parent="tmdb", default="en"), + "expiration": check_for_attribute(self.data, "cache_expiration", parent="tmdb", var_type="int", default=60) }) logger.info(f"TMDb Connection {'Failed' if self.TMDb is None else 'Successful'}") else: diff --git a/modules/meta.py b/modules/meta.py index b5d0c2739..3c87b648a 100644 --- a/modules/meta.py +++ b/modules/meta.py @@ -303,8 +303,8 @@ def _check_dict(check_dict): logger.ghost(f"Processing: {i}/{len(all_items)} {item.title}") tmdb_id, tvdb_id, imdb_id = library.get_ids(item) tmdb_item = config.TMDb.get_item(item, tmdb_id, tvdb_id, imdb_id, is_movie=True) - if tmdb_item and tmdb_item.collection and tmdb_item.collection.id not in exclude and tmdb_item.collection.name not in exclude: - auto_list[str(tmdb_item.collection.id)] = tmdb_item.collection.name + if tmdb_item and tmdb_item.collection_id and tmdb_item.collection_id not in exclude and tmdb_item.collection_name not in exclude: + auto_list[str(tmdb_item.collection_id)] = tmdb_item.collection_name logger.exorcise() elif auto_type == "original_language": if not all_items: @@ -313,8 +313,8 @@ def _check_dict(check_dict): logger.ghost(f"Processing: {i}/{len(all_items)} {item.title}") tmdb_id, tvdb_id, imdb_id = library.get_ids(item) tmdb_item = config.TMDb.get_item(item, tmdb_id, tvdb_id, imdb_id, is_movie=library.type == "Movie") - if tmdb_item and tmdb_item.original_language and tmdb_item.original_language.iso_639_1 not in exclude and tmdb_item.original_language.english_name not in exclude: - auto_list[tmdb_item.original_language.iso_639_1] = tmdb_item.original_language.english_name + if tmdb_item and tmdb_item.language_iso and tmdb_item.language_iso not in exclude and tmdb_item.language_name not in exclude: + auto_list[tmdb_item.language_iso] = tmdb_item.language_name logger.exorcise() default_title_format = "<> <>s" elif auto_type == "origin_country": @@ -324,8 +324,8 @@ def _check_dict(check_dict): logger.ghost(f"Processing: {i}/{len(all_items)} {item.title}") tmdb_id, tvdb_id, imdb_id = library.get_ids(item) tmdb_item = config.TMDb.get_item(item, tmdb_id, tvdb_id, imdb_id, is_movie=library.type == "Movie") - if tmdb_item and tmdb_item.origin_countries: - for country in tmdb_item.origin_countries: + if tmdb_item and tmdb_item.countries: + for country in tmdb_item.countries: if country.iso_3166_1 not in exclude and country.name not in exclude: auto_list[country.iso_3166_1] = country.name logger.exorcise() @@ -662,18 +662,14 @@ def add_edit(name, current_item, group, alias, key=None, value=None, var_type="s genres = [] if tmdb_item: originally_available = datetime.strftime(tmdb_item.release_date if tmdb_is_movie else tmdb_item.first_air_date, "%Y-%m-%d") - if tmdb_is_movie and tmdb_item.original_title != tmdb_item.title: + + if tmdb_item.original_title != tmdb_item.title: original_title = tmdb_item.original_title - elif not tmdb_is_movie and tmdb_item.original_name != tmdb_item.name: - original_title = tmdb_item.original_name rating = tmdb_item.vote_average - if tmdb_is_movie and tmdb_item.companies: - studio = tmdb_item.companies[0].name - elif not tmdb_is_movie and tmdb_item.networks: - studio = tmdb_item.networks[0].name + studio = tmdb_item.studio tagline = tmdb_item.tagline if len(tmdb_item.tagline) > 0 else None summary = tmdb_item.overview - genres = [genre.name for genre in tmdb_item.genres] + genres = tmdb_item.genres edits = {} add_edit("title", item, meta, methods, value=title) diff --git a/modules/tmdb.py b/modules/tmdb.py index 34e99e433..2544fc766 100644 --- a/modules/tmdb.py +++ b/modules/tmdb.py @@ -57,11 +57,100 @@ discover_tv_sort = ["vote_average.desc", "vote_average.asc", "first_air_date.desc", "first_air_date.asc", "popularity.desc", "popularity.asc"] discover_monetization_types = ["flatrate", "free", "ads", "rent", "buy"] + +class TMDbCountry: + def __init__(self, data): + self.iso_3166_1 = data.split(":")[0] if isinstance(data, str) else data.iso_3166_1 + self.name = data.split(":")[1] if isinstance(data, str) else data.name + + def __repr__(self): + return f"{self.iso_3166_1}:{self.name}" + + +class TMDbSeason: + def __init__(self, data): + self.season_number = data.split(":")[0] if isinstance(data, str) else data.season_number + self.name = data.split(":")[1] if isinstance(data, str) else data.name + + def __repr__(self): + return f"{self.season_number}:{self.name}" + + +class TMDBObj: + def __init__(self, tmdb, tmdb_id, ignore_cache=False): + self._tmdb = tmdb + self.tmdb_id = tmdb_id + self.ignore_cache = ignore_cache + + def _load(self, data): + self.title = data["title"] if isinstance(data, dict) else data.title + self.tagline = data["tagline"] if isinstance(data, dict) else data.tagline + self.overview = data["overview"] if isinstance(data, dict) else data.overview + self.imdb_id = data["imdb_id"] if isinstance(data, dict) else data.imdb_id + self.poster_url = data["poster_url"] if isinstance(data, dict) else data.poster_url + self.backdrop_url = data["backdrop_url"] if isinstance(data, dict) else data.backdrop_url + self.vote_count = data["vote_count"] if isinstance(data, dict) else data.vote_count + self.vote_average = data["vote_average"] if isinstance(data, dict) else data.vote_average + self.language_iso = data["language_iso"] if isinstance(data, dict) else data.original_language.iso_639_1 if data.original_language else None + self.language_name = data["language_name"] if isinstance(data, dict) else data.original_language.english_name if data.original_language else None + self.genres = data["genres"].split("|") if isinstance(data, dict) else [g.name for g in data.genres] + self.keywords = data["keywords"].split("|") if isinstance(data, dict) else [g.name for g in data.keywords] + + +class TMDbMovie(TMDBObj): + def __init__(self, tmdb, tmdb_id, ignore_cache=False): + super().__init__(tmdb, tmdb_id, ignore_cache=ignore_cache) + expired = None + data = None + if self._tmdb.config.Cache and not ignore_cache: + data, expired = self._tmdb.config.Cache.query_tmdb_movie(tmdb_id, self._tmdb.expiration) + if expired or not data: + data = self._tmdb.TMDb.movie(self.tmdb_id, partial="external_ids,keywords") + super()._load(data) + + self.original_title = data["original_title"] if isinstance(data, dict) else data.original_title + self.release_date = data["release_date"] if isinstance(data, dict) else data.release_date + self.studio = data["studio"] if isinstance(data, dict) else data.companies[0].name + self.collection_id = data["collection_id"] if isinstance(data, dict) else data.collection.id if data.collection else None + self.collection_name = data["collection_name"] if isinstance(data, dict) else data.collection.name if data.collection else None + + if self._tmdb.config.Cache and not ignore_cache: + self._tmdb.config.Cache.update_tmdb_movie(expired, self, self._tmdb.expiration) + + +class TMDbShow(TMDBObj): + def __init__(self, tmdb, tmdb_id, ignore_cache=False): + super().__init__(tmdb, tmdb_id, ignore_cache=ignore_cache) + expired = None + data = None + if self._tmdb.config.Cache and not ignore_cache: + data, expired = self._tmdb.config.Cache.query_tmdb_show(tmdb_id, self._tmdb.expiration) + if expired or not data: + data = self._tmdb.TMDb.tv_show(self.tmdb_id, partial="external_ids,keywords") + super()._load(data) + + self.original_title = data["original_title"] if isinstance(data, dict) else data.original_name + self.first_air_date = data["first_air_date"] if isinstance(data, dict) else data.first_air_date + self.last_air_date = data["last_air_date"] if isinstance(data, dict) else data.last_air_date + self.status = data["status"] if isinstance(data, dict) else data.status + self.type = data["type"] if isinstance(data, dict) else data.type + self.studio = data["studio"] if isinstance(data, dict) else data.networks[0].name + self.tvdb_id = data["tvdb_id"] if isinstance(data, dict) else data.tvdb_id + loop = data["countries"].split("|") if isinstance(data, dict) else data.origin_countries + self.countries = [TMDbCountry(c) for c in loop] + loop = data["seasons"].split("|") if isinstance(data, dict) else data.seasons + self.seasons = [TMDbSeason(s) for s in loop] + + if self._tmdb.config.Cache and not ignore_cache: + self._tmdb.config.Cache.update_tmdb_show(expired, self, self._tmdb.expiration) + + class TMDb: def __init__(self, config, params): self.config = config self.apikey = params["apikey"] self.language = params["language"] + self.expiration = params["expiration"] logger.secret(self.apikey) try: self.TMDb = TMDbAPIs(self.apikey, language=self.language, session=self.config.session) @@ -69,7 +158,7 @@ def __init__(self, config, params): raise Failed(f"TMDb Error: {e}") def convert_from(self, tmdb_id, convert_to, is_movie): - item = self.get_movie(tmdb_id, partial="external_ids") if is_movie else self.get_show(tmdb_id, partial="external_ids") + item = self.get_movie(tmdb_id) if is_movie else self.get_show(tmdb_id) check_id = item.tvdb_id if convert_to == "tvdb_id" and not is_movie else item.imdb_id if not check_id: raise Failed(f"TMDb Error: No {convert_to.upper().replace('B_', 'b ')} found for TMDb ID {tmdb_id}") @@ -106,12 +195,12 @@ def get_movie_show_or_collection(self, tmdb_id, is_movie): except Failed: raise Failed(f"TMDb Error: No Movie or Collection found for TMDb ID {tmdb_id}") else: return self.get_show(tmdb_id) - def get_movie(self, tmdb_id, partial=None): - try: return self.TMDb.movie(tmdb_id, partial=partial) + def get_movie(self, tmdb_id): + try: return TMDbMovie(self, tmdb_id) except TMDbException as e: raise Failed(f"TMDb Error: No Movie found for TMDb ID {tmdb_id}: {e}") - def get_show(self, tmdb_id, partial=None): - try: return self.TMDb.tv_show(tmdb_id, partial=partial) + def get_show(self, tmdb_id): + try: return TMDbShow(self, tmdb_id) except TMDbException as e: raise Failed(f"TMDb Error: No Show found for TMDb ID {tmdb_id}: {e}") def get_collection(self, tmdb_id, partial=None): @@ -219,7 +308,7 @@ def get_tmdb_ids(self, method, data, is_movie): tmdb_name = collection.name ids = [(t.id, "tmdb") for t in collection.movies] elif method == "tmdb_show": - tmdb_name = self.get_show(tmdb_id).name + tmdb_name = self.get_show(tmdb_id).title ids.append((tmdb_id, "tmdb_show")) else: person = self.get_person(tmdb_id, partial="movie_credits,tv_credits") diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 2b183f91d..93a4199a9 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -533,8 +533,8 @@ def library_operations(config, library): else: logger.info(f"{item.title[:25]:<25} | No IMDb ID for Guid: {item.guid}") - if library.tmdb_collections and tmdb_item and tmdb_item.collection: - tmdb_collections[tmdb_item.collection.id] = tmdb_item.collection.name + if library.tmdb_collections and tmdb_item and tmdb_item.collection_id: + tmdb_collections[tmdb_item.collection_id] = tmdb_item.collection_name def get_rating(attribute): if tmdb_item and attribute == "tmdb": @@ -565,7 +565,7 @@ def get_rating(attribute): if library.mass_genre_update: try: if tmdb_item and library.mass_genre_update == "tmdb": - new_genres = [genre.name for genre in tmdb_item.genres] + new_genres = tmdb_item.genres elif omdb_item and library.mass_genre_update == "omdb": new_genres = omdb_item.genres elif tvdb_item and library.mass_genre_update == "tvdb": From c281ad9ed87d167b19f32de95c9dff9518850be4 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Sun, 13 Mar 2022 15:40:17 -0400 Subject: [PATCH 06/32] [6] small fixes --- VERSION | 2 +- docs/_static/custom.css | 4 ++++ docs/metadata/filters.md | 2 +- modules/builder.py | 4 ++-- modules/config.py | 4 ++-- 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/VERSION b/VERSION index bc2cbf693..fa9c14dce 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop5 +1.16.1-develop6 diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 4b4bf1501..db3d08783 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -168,4 +168,8 @@ p { } .tab-set { width: auto !important; +} +.highlighted { + background-color: #ac0ce3 !important; + color: #FFFFFF !important; } \ No newline at end of file diff --git a/docs/metadata/filters.md b/docs/metadata/filters.md index a462af74c..8bf259405 100644 --- a/docs/metadata/filters.md +++ b/docs/metadata/filters.md @@ -76,7 +76,7 @@ Tag filters can take multiple values as a **list or a comma-separated string**. | `resolution` | Uses the resolution tag to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | | `audio_language` | Uses the audio language tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | | `subtitle_language` | Uses the subtitle language tags to match | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | -| `original_language`1 | Uses TMDb original language [ISO 639-1 codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) to match
Example: `original_language: en, ko` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | +| `original_language`1 | Uses TMDb original language [ISO 639-1 codes](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) to match
Example: `original_language: en, ko` | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | | `origin_country`1 | Uses TMDb origin country [ISO 3166-1 alpha-2 codes](https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2) to match
Example: `origin_country: us` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | `tmdb_status`1 | Uses TMDb Status to match
**Values:** `returning`, `planned`, `production`, `ended`, `canceled`, `pilot` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | | `tmdb_type`1 | Uses TMDb Type to match
**Values:** `documentary`, `news`, `production`, `miniseries`, `reality`, `scripted`, `talk_show`, `video` | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ | diff --git a/modules/builder.py b/modules/builder.py index bdcab5da7..453024884 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -2399,10 +2399,10 @@ def get_summary(summary_method, summaries): if "visible_library" in self.details and self.details["visible_library"] != visibility["library"]: visible_library = self.details["visible_library"] - if "visible_home" in self.details and self.details["visible_home"] != visibility["library"]: + if "visible_home" in self.details and self.details["visible_home"] != visibility["home"]: visible_home = self.details["visible_home"] - if "visible_shared" in self.details and self.details["visible_shared"] != visibility["library"]: + if "visible_shared" in self.details and self.details["visible_shared"] != visibility["shared"]: visible_shared = self.details["visible_shared"] if visible_library is not None or visible_home is not None or visible_shared is not None: diff --git a/modules/config.py b/modules/config.py index 566ed5fab..8ead5ebb9 100644 --- a/modules/config.py +++ b/modules/config.py @@ -381,7 +381,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No try: self.OMDb = OMDb(self, { "apikey": check_for_attribute(self.data, "apikey", parent="omdb", throw=True), - "expiration": check_for_attribute(self.data, "cache_expiration", parent="settings", var_type="int", default=60) + "expiration": check_for_attribute(self.data, "cache_expiration", parent="omdb", var_type="int", default=60) }) except Failed as e: self.errors.append(e) @@ -398,7 +398,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No try: self.Mdblist.add_key( check_for_attribute(self.data, "apikey", parent="mdblist", throw=True), - check_for_attribute(self.data, "cache_expiration", parent="settings", var_type="int", default=60) + check_for_attribute(self.data, "cache_expiration", parent="mdblist", var_type="int", default=60) ) logger.info("Mdblist Connection Successful") except Failed as e: From 38ffe62375799813acc27f85d89639ac873e5c2d Mon Sep 17 00:00:00 2001 From: YozoraXCII <96386153+YozoraXCII@users.noreply.github.com> Date: Sun, 13 Mar 2022 20:44:42 +0000 Subject: [PATCH 07/32] [7] Resolve content_rating_mapper issues --- VERSION | 2 +- modules/config.py | 15 +++++++++++++++ modules/library.py | 3 ++- plex_meta_manager.py | 24 +++++++++++++++--------- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/VERSION b/VERSION index fa9c14dce..9504e7dcd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop6 +1.16.1-develop7 diff --git a/modules/config.py b/modules/config.py index 8ead5ebb9..9b8a0f4dc 100644 --- a/modules/config.py +++ b/modules/config.py @@ -593,6 +593,7 @@ def check_dict(attr): "name": str(lib["library_name"]) if lib and "library_name" in lib and lib["library_name"] else str(library_name), "tmdb_collections": None, "genre_mapper": None, + "content_rating_mapper": None, "radarr_remove_by_tag": None, "sonarr_remove_by_tag": None, "mass_collection_mode": None, @@ -735,6 +736,20 @@ def check_dict(attr): params["genre_mapper"][old_genre] = new_genre else: logger.error("Config Error: genre_mapper is blank") + if "content_rating_mapper" in lib["operations"]: + if lib["operations"]["content_rating_mapper"] and isinstance(lib["operations"]["content_rating_mapper"], dict): + params["content_rating_mapper"] = {} + for new_rating, old_ratings in lib["operations"]["content_rating_mapper"].items(): + if old_ratings is None: + params["content_rating_mapper"][new_rating] = old_ratings + else: + for old_rating in util.get_list(old_ratings): + if old_rating == new_rating: + logger.error("Config Error: Content Ratings cannot be mapped to themselves") + else: + params["content_rating_mapper"][old_rating] = new_rating + else: + logger.error("Config Error: content_rating_mapper is blank") if "genre_collections" in lib["operations"]: params["genre_collections"] = { "exclude_genres": [], diff --git a/modules/library.py b/modules/library.py index 9ad5e2d02..6e57f27c6 100644 --- a/modules/library.py +++ b/modules/library.py @@ -83,6 +83,7 @@ def __init__(self, config, params): self.tmdb_collections = params["tmdb_collections"] self.genre_collections = params["genre_collections"] self.genre_mapper = params["genre_mapper"] + self.content_rating_mapper = params["content_rating_mapper"] self.error_webhooks = params["error_webhooks"] self.changes_webhooks = params["changes_webhooks"] self.split_duplicates = params["split_duplicates"] # TODO: Here or just in Plex? @@ -94,7 +95,7 @@ def __init__(self, config, params): self.items_library_operation = True if self.assets_for_all or self.mass_genre_update or self.mass_audience_rating_update \ or self.mass_critic_rating_update or self.mass_content_rating_update or self.mass_trakt_rating_update \ - or self.genre_mapper or self.tmdb_collections or self.radarr_add_all_existing or self.sonarr_add_all_existing else False + or self.genre_mapper or self.content_rating_mapper or self.tmdb_collections or self.radarr_add_all_existing or self.sonarr_add_all_existing else False self.library_operation = True if self.items_library_operation or self.delete_unmanaged_collections or self.delete_collections_with_less \ or self.radarr_remove_by_tag or self.sonarr_remove_by_tag or self.mass_collection_mode \ or self.genre_collections or self.show_unmanaged or self.metadata_backup or self.update_blank_track_titles else False diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 93a4199a9..97777c2b5 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -418,6 +418,7 @@ def library_operations(config, library): logger.debug(f"TMDb Collections: {library.tmdb_collections}") logger.debug(f"Genre Collections: {library.genre_collections}") logger.debug(f"Genre Mapper: {library.genre_mapper}") + logger.debug(f"Content Rating Mapper: {library.content_rating_mapper}") logger.debug(f"Metadata Backup: {library.metadata_backup}") logger.debug(f"Item Operation: {library.items_library_operation}") logger.debug("") @@ -613,17 +614,22 @@ def get_rating(attribute): except Failed: pass - if library.genre_mapper: + if library.genre_mapper or library.content_rating_mapper: try: - adds = [] - deletes = [] library.reload(item) - for genre in item.genres: - if genre.tag in library.genre_mapper: - deletes.append(genre.tag) - if library.genre_mapper[genre.tag]: - adds.append(library.genre_mapper[genre.tag]) - library.edit_tags("genre", item, add_tags=adds, remove_tags=deletes) + if library.genre_mapper: + adds = [] + deletes = [] + for genre in item.genres: + if genre.tag in library.genre_mapper: + deletes.append(genre.tag) + if library.genre_mapper[genre.tag]: + adds.append(library.genre_mapper[genre.tag]) + library.edit_tags("genre", item, add_tags=adds, remove_tags=deletes) + if library.content_rating_mapper: + if item.contentRating in library.content_rating_mapper: + library.edit_query(item, {"contentRating.value": library.content_rating_mapper[item.contentRating], "contentRating.locked": 1}) + logger.info(f"{item.title[:25]:<25} | Content Rating | {library.content_rating_mapper[item.contentRating]}") except Failed: pass From 18e7c8446919aeee95ce9873fc302b9ffe4297e1 Mon Sep 17 00:00:00 2001 From: YozoraXCII <96386153+YozoraXCII@users.noreply.github.com> Date: Sun, 13 Mar 2022 20:44:42 +0000 Subject: [PATCH 08/32] [7] Resolve content_rating_mapper issues --- VERSION | 2 +- modules/config.py | 15 +++++++++++++++ modules/library.py | 3 ++- plex_meta_manager.py | 24 +++++++++++++++--------- 4 files changed, 33 insertions(+), 11 deletions(-) diff --git a/VERSION b/VERSION index fa9c14dce..9504e7dcd 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop6 +1.16.1-develop7 diff --git a/modules/config.py b/modules/config.py index 8ead5ebb9..9b8a0f4dc 100644 --- a/modules/config.py +++ b/modules/config.py @@ -593,6 +593,7 @@ def check_dict(attr): "name": str(lib["library_name"]) if lib and "library_name" in lib and lib["library_name"] else str(library_name), "tmdb_collections": None, "genre_mapper": None, + "content_rating_mapper": None, "radarr_remove_by_tag": None, "sonarr_remove_by_tag": None, "mass_collection_mode": None, @@ -735,6 +736,20 @@ def check_dict(attr): params["genre_mapper"][old_genre] = new_genre else: logger.error("Config Error: genre_mapper is blank") + if "content_rating_mapper" in lib["operations"]: + if lib["operations"]["content_rating_mapper"] and isinstance(lib["operations"]["content_rating_mapper"], dict): + params["content_rating_mapper"] = {} + for new_rating, old_ratings in lib["operations"]["content_rating_mapper"].items(): + if old_ratings is None: + params["content_rating_mapper"][new_rating] = old_ratings + else: + for old_rating in util.get_list(old_ratings): + if old_rating == new_rating: + logger.error("Config Error: Content Ratings cannot be mapped to themselves") + else: + params["content_rating_mapper"][old_rating] = new_rating + else: + logger.error("Config Error: content_rating_mapper is blank") if "genre_collections" in lib["operations"]: params["genre_collections"] = { "exclude_genres": [], diff --git a/modules/library.py b/modules/library.py index 9ad5e2d02..6e57f27c6 100644 --- a/modules/library.py +++ b/modules/library.py @@ -83,6 +83,7 @@ def __init__(self, config, params): self.tmdb_collections = params["tmdb_collections"] self.genre_collections = params["genre_collections"] self.genre_mapper = params["genre_mapper"] + self.content_rating_mapper = params["content_rating_mapper"] self.error_webhooks = params["error_webhooks"] self.changes_webhooks = params["changes_webhooks"] self.split_duplicates = params["split_duplicates"] # TODO: Here or just in Plex? @@ -94,7 +95,7 @@ def __init__(self, config, params): self.items_library_operation = True if self.assets_for_all or self.mass_genre_update or self.mass_audience_rating_update \ or self.mass_critic_rating_update or self.mass_content_rating_update or self.mass_trakt_rating_update \ - or self.genre_mapper or self.tmdb_collections or self.radarr_add_all_existing or self.sonarr_add_all_existing else False + or self.genre_mapper or self.content_rating_mapper or self.tmdb_collections or self.radarr_add_all_existing or self.sonarr_add_all_existing else False self.library_operation = True if self.items_library_operation or self.delete_unmanaged_collections or self.delete_collections_with_less \ or self.radarr_remove_by_tag or self.sonarr_remove_by_tag or self.mass_collection_mode \ or self.genre_collections or self.show_unmanaged or self.metadata_backup or self.update_blank_track_titles else False diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 93a4199a9..97777c2b5 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -418,6 +418,7 @@ def library_operations(config, library): logger.debug(f"TMDb Collections: {library.tmdb_collections}") logger.debug(f"Genre Collections: {library.genre_collections}") logger.debug(f"Genre Mapper: {library.genre_mapper}") + logger.debug(f"Content Rating Mapper: {library.content_rating_mapper}") logger.debug(f"Metadata Backup: {library.metadata_backup}") logger.debug(f"Item Operation: {library.items_library_operation}") logger.debug("") @@ -613,17 +614,22 @@ def get_rating(attribute): except Failed: pass - if library.genre_mapper: + if library.genre_mapper or library.content_rating_mapper: try: - adds = [] - deletes = [] library.reload(item) - for genre in item.genres: - if genre.tag in library.genre_mapper: - deletes.append(genre.tag) - if library.genre_mapper[genre.tag]: - adds.append(library.genre_mapper[genre.tag]) - library.edit_tags("genre", item, add_tags=adds, remove_tags=deletes) + if library.genre_mapper: + adds = [] + deletes = [] + for genre in item.genres: + if genre.tag in library.genre_mapper: + deletes.append(genre.tag) + if library.genre_mapper[genre.tag]: + adds.append(library.genre_mapper[genre.tag]) + library.edit_tags("genre", item, add_tags=adds, remove_tags=deletes) + if library.content_rating_mapper: + if item.contentRating in library.content_rating_mapper: + library.edit_query(item, {"contentRating.value": library.content_rating_mapper[item.contentRating], "contentRating.locked": 1}) + logger.info(f"{item.title[:25]:<25} | Content Rating | {library.content_rating_mapper[item.contentRating]}") except Failed: pass From 04574c3548e4e9055fca897272fd163c9bd1589b Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Sun, 13 Mar 2022 16:49:50 -0400 Subject: [PATCH 09/32] [8] allow dict lists --- VERSION | 2 +- modules/util.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 9504e7dcd..7d01a8dfc 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop7 +1.16.1-develop8 diff --git a/modules/util.py b/modules/util.py index 85e2a83f6..5906fc798 100644 --- a/modules/util.py +++ b/modules/util.py @@ -111,7 +111,7 @@ def get_list(data, lower=False, upper=False, split=True, int_list=False): elif int_list is True: try: return [int(str(d).strip()) for d in list_data] except ValueError: return [] - else: return [str(d).strip() for d in list_data] + else: return [d if isinstance(d, dict) else str(d).strip() for d in list_data] def get_int_list(data, id_type): int_values = [] From c0dd20eb9a69583a2dd06fbb9c8a43244c805fe2 Mon Sep 17 00:00:00 2001 From: Frazzer951 Date: Sun, 13 Mar 2022 14:06:31 -0700 Subject: [PATCH 10/32] Feature: Add Builder for Userlists from AniList --- modules/anilist.py | 33 +++++++++++++++++++++++++++++++-- modules/builder.py | 10 +++++++++- 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/modules/anilist.py b/modules/anilist.py index ac571ced8..965dbfa54 100644 --- a/modules/anilist.py +++ b/modules/anilist.py @@ -4,8 +4,12 @@ logger = util.logger -builders = ["anilist_id", "anilist_popular", "anilist_trending", "anilist_relations", "anilist_studio", "anilist_top_rated", "anilist_search"] -pretty_names = {"score": "Average Score", "popular": "Popularity", "trending": "Trending"} +builders = ["anilist_id", "anilist_popular", "anilist_trending", "anilist_relations", "anilist_studio", "anilist_top_rated", "anilist_search", "anilist_userlist"] +pretty_names = { + "score": "Average Score", "popular": "Popularity", "trending": "Trending", "CURRENT": "Currently Watching", + "COMPLETED": "Completed", "PAUSED": "Paused", "DROPPED": "Dropped", "PLANNING": "Planning", "SCORE": "Score", + "UPDATED_TIME": "Updated Time", "STARTED_ON": "Start Date", "MEDIA_TITLE_NATIVE": "Title" + } attr_translation = { "year": "seasonYear", "adult": "isAdult", "start": "startDate", "end": "endDate", "tag_category": "tagCategory", "score": "averageScore", "min_tag_percent": "minimumTagRank", "country": "countryOfOrigin", @@ -20,6 +24,17 @@ no_mod_searches = ["search", "season", "year", "adult", "min_tag_percent", "limit", "sort_by", "source", "country"] searches = mod_searches + no_mod_searches sort_options = {"score": "SCORE_DESC", "popular": "POPULARITY_DESC", "trending": "TRENDING_DESC"} +userlist_sort_options = ["score", "last_updated", "title", "start_date"] +userlist_sort_translation = { + "score": "SCORE", "last_updated": "UPDATED_TIME", + "title": "MEDIA_TITLE_NATIVE", "start_date": "STARTED_ON" +} +userlist_status = ["watching", "completed", "paused", "dropped", "planning"] +userlist_status_translation = { + "watching": "CURRENT", "completed": "COMPLETED", + "paused": "PAUSED", "dropped" :"DROPPED", + "planning": "PLANNING" +} media_season = {"winter": "WINTER", "spring": "SPRING", "summer": "SUMMER", "fall": "FALL"} media_format = {"tv": "TV", "short": "TV_SHORT", "movie": "MOVIE", "special": "SPECIAL", "ova": "OVA", "ona": "ONA", "music": "MUSIC"} media_status = {"finished": "FINISHED", "airing": "RELEASING", "not_yet_aired": "NOT_YET_RELEASED", "cancelled": "CANCELLED", "hiatus": "HIATUS"} @@ -210,6 +225,17 @@ def _relations(self, anilist_id, ignore_ids=None): return anilist_ids, ignore_ids, name + def _userlist(self, username, status, sort_by): + query = "query ($userName: String, $status: MediaListStatus) {MediaListCollection (userName: $userName, status: $status, type: ANIME) {lists {status entries {media{id title{romaji english}}}}}}" + variables = {"userName": username, "status": status, "sort": sort_by} + json_obj = self._request(query, variables) + lists = json_obj['data']['MediaListCollection']['lists'] + anilist_ids = [] + for list in lists: + for media in list['entries']: + anilist_ids.append(media['media']['id']) + return anilist_ids + def validate(self, name, data): valid = [] for d in util.get_list(data): @@ -243,6 +269,9 @@ def get_anilist_ids(self, method, data): elif method == "anilist_relations": anilist_ids, _, name = self._relations(data) logger.info(f"Processing AniList Relations: ({data}) {name} ({len(anilist_ids)} Anime)") + elif method == "anilist_userlist": + anilist_ids = self._userlist(data["username"], data["status"], data['sort_by']) + logger.info(f"Processing AniList Userlist: Anime from {data['username']}'s {pretty_names[data['status']]} list sorted by {pretty_names[data['sort_by']]}") else: if method == "anilist_popular": data = {"limit": data, "popularity.gt": 3, "sort_by": "popular"} diff --git a/modules/builder.py b/modules/builder.py index 453024884..8c19c79f2 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -178,7 +178,7 @@ "trakt_recommended_personal", "trakt_recommended_daily", "trakt_recommended_weekly", "trakt_recommended_monthly", "trakt_recommended_yearly", "trakt_recommended_all", "trakt_watched_daily", "trakt_watched_weekly", "trakt_watched_monthly", "trakt_watched_yearly", "trakt_watched_all", "tautulli_popular", "tautulli_watched", "mdblist_list", "letterboxd_list", "icheckmovies_list", - "anilist_top_rated", "anilist_popular", "anilist_trending", "anilist_search", + "anilist_top_rated", "anilist_popular", "anilist_trending", "anilist_search", "anilist_userlist", "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" ] @@ -955,6 +955,14 @@ def _anilist(self, method_name, method_data): self.builders.append((method_name, anilist_id)) elif method_name in ["anilist_popular", "anilist_trending", "anilist_top_rated"]: self.builders.append((method_name, util.parse(self.Type, method_name, method_data, datatype="int", default=10))) + elif method_name == "anilist_userlist": + for dict_data in util.parse(self.Type, method_name, method_data, datatype="listdict"): + dict_methods = {dm.lower(): dm for dm in dict_data} + self.builders.append((method_name, { + "username": util.parse(self.Type, "username", dict_data, methods=dict_methods, parent=method_name), + "status": util.parse(self.Type, "status", dict_data, methods=dict_methods, parent=method_name, default="watching", options=anilist.userlist_status, translation=anilist.userlist_status_translation), + "sort_by": util.parse(self.Type, "sort_by", dict_data, methods=dict_methods, parent=method_name, default="score", options=anilist.userlist_sort_options, translation=anilist.userlist_sort_translation), + })) elif method_name == "anilist_search": if self.current_time.month in [12, 1, 2]: current_season = "winter" elif self.current_time.month in [3, 4, 5]: current_season = "spring" From 206e38a264d601bc4aa60f17052e92a515a43d7e Mon Sep 17 00:00:00 2001 From: Frazzer951 Date: Sun, 13 Mar 2022 15:15:23 -0700 Subject: [PATCH 11/32] add documentation for AniList Userlist --- docs/metadata/builders/anilist.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/docs/metadata/builders/anilist.md b/docs/metadata/builders/anilist.md index f316e2d6c..4a55b3545 100644 --- a/docs/metadata/builders/anilist.md +++ b/docs/metadata/builders/anilist.md @@ -13,6 +13,7 @@ No configuration is required for these builders. | [`anilist_studio`](#anilist-studio) | Finds all anime specified by the AniList Studio ID | ✅ | ✅ | ❌ | | [`anilist_id`](#anilist-id) | Finds the anime specified by the AniList ID | ✅ | ✅ | ❌ | | [`anilist_search`](#anilist-search) | Finds the anime specified by the AniList search parameters provided | ✅ | ✅ | ✅ | +| [`anilist_userlist`](#anilist-userlist) | Finds anime in AniList User's Anime list the options are detailed below | ✅ | ✅ | ✅ | ## AniList Top Rated Anime @@ -229,3 +230,27 @@ collections: collection_order: custom sync_mode: sync ``` + +## AniList UserList + +Gets anime in AniList User's Anime list. The different sub-attributes are detailed below. The only required attribute is `username` + +The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order. + +| Attribute | Description | +|:-----------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `username` | **Description:** A user's AniList Username | +| `status` | **Description:** Status to search for
**Default:** `watching`
**Values:**
`watching`Currently Watching List
`completed`Completed List
`paused`Paused List
`dropped`Dropped List
`planning`Planning
| +| `sort_by` | **Description:** Sort Order to return
**Default:** `score`
**Values:**
`score`Sort by Score
`last_updated`Sort by Last Updated
`title`Sort by Anime Title
`start_date`Sort by Start Date
| + + +```yaml +collections: + Currently Watching Anime: + anilist_userlist: + username: "Username" + status: watching + sort_by: score + collection_order: custom + sync_mode: sync +``` From 54383bafb0ff9b604ccacc5cd0b429fc11df2b5d Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Sun, 13 Mar 2022 22:49:55 -0400 Subject: [PATCH 12/32] [10] update anilist_userlist --- VERSION | 2 +- docs/metadata/builders/anilist.md | 53 +++++++++++----------- modules/anilist.py | 73 ++++++++++++++++++++----------- modules/builder.py | 8 ++-- 4 files changed, 79 insertions(+), 57 deletions(-) diff --git a/VERSION b/VERSION index 69bcdfaf9..bdf223dec 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop9 +1.16.1-develop10 diff --git a/docs/metadata/builders/anilist.md b/docs/metadata/builders/anilist.md index 4a55b3545..e1c785170 100644 --- a/docs/metadata/builders/anilist.md +++ b/docs/metadata/builders/anilist.md @@ -12,8 +12,8 @@ No configuration is required for these builders. | [`anilist_relations`](#anilist-relations) | Finds the anime specified by the AniList ID and every relation in its relation tree except Character and Other relations | ✅ | ✅ | ❌ | | [`anilist_studio`](#anilist-studio) | Finds all anime specified by the AniList Studio ID | ✅ | ✅ | ❌ | | [`anilist_id`](#anilist-id) | Finds the anime specified by the AniList ID | ✅ | ✅ | ❌ | +| [`anilist_userlist`](#anilist-userlist) | Finds the anime in AniList User's Anime list the options are detailed below | ✅ | ✅ | ✅ | | [`anilist_search`](#anilist-search) | Finds the anime specified by the AniList search parameters provided | ✅ | ✅ | ✅ | -| [`anilist_userlist`](#anilist-userlist) | Finds anime in AniList User's Anime list the options are detailed below | ✅ | ✅ | ✅ | ## AniList Top Rated Anime @@ -99,6 +99,31 @@ collections: anilist_id: 23, 219 ``` +## AniList UserList + +Gets anime in AniList User's Anime list. The different sub-attributes are detailed below. + +Both `username` and `list_name` are required. + +The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order. + +| Attribute | Description | +|:------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `username` | **Description:** A user's AniList Username | +| `list_name` | **Description:** A user's AniList List Name | +| `sort_by` | **Description:** Sort Order to return
**Default:** `score`
**Values:**
`score`Sort by User Score
`popularity`Sort by Popularity
`status`Sort by Status
`progress`Sort by Progress
`last_updated`Sort by Last Updated
`last_added`Sort by Last Added
`start_date`Sort by Start Date
`completed_date`Sort by Completed Date
| + +```yaml +collections: + Currently Watching Anime: + anilist_userlist: + username: Username + list_name: Watching + sort_by: score + collection_order: custom + sync_mode: sync +``` + ## AniList Search Finds the anime specified by the AniList Search the options are detailed below. @@ -229,28 +254,4 @@ collections: sort_by: popular collection_order: custom sync_mode: sync -``` - -## AniList UserList - -Gets anime in AniList User's Anime list. The different sub-attributes are detailed below. The only required attribute is `username` - -The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order. - -| Attribute | Description | -|:-----------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `username` | **Description:** A user's AniList Username | -| `status` | **Description:** Status to search for
**Default:** `watching`
**Values:**
`watching`Currently Watching List
`completed`Completed List
`paused`Paused List
`dropped`Dropped List
`planning`Planning
| -| `sort_by` | **Description:** Sort Order to return
**Default:** `score`
**Values:**
`score`Sort by Score
`last_updated`Sort by Last Updated
`title`Sort by Anime Title
`start_date`Sort by Start Date
| - - -```yaml -collections: - Currently Watching Anime: - anilist_userlist: - username: "Username" - status: watching - sort_by: score - collection_order: custom - sync_mode: sync -``` +``` \ No newline at end of file diff --git a/modules/anilist.py b/modules/anilist.py index 965dbfa54..68f85f16d 100644 --- a/modules/anilist.py +++ b/modules/anilist.py @@ -5,11 +5,11 @@ logger = util.logger builders = ["anilist_id", "anilist_popular", "anilist_trending", "anilist_relations", "anilist_studio", "anilist_top_rated", "anilist_search", "anilist_userlist"] -pretty_names = { - "score": "Average Score", "popular": "Popularity", "trending": "Trending", "CURRENT": "Currently Watching", - "COMPLETED": "Completed", "PAUSED": "Paused", "DROPPED": "Dropped", "PLANNING": "Planning", "SCORE": "Score", - "UPDATED_TIME": "Updated Time", "STARTED_ON": "Start Date", "MEDIA_TITLE_NATIVE": "Title" - } +pretty_names = {"score": "Average Score", "popular": "Popularity", "trending": "Trending"} +pretty_user = { + "status": "Status", "score": "User Score", "progress": "Progress", "last_updated": "Last Updated", + "last_added": "Last Added", "start_date": "Start Date", "completed_date": "Completed Date", "popularity": "Popularity" +} attr_translation = { "year": "seasonYear", "adult": "isAdult", "start": "startDate", "end": "endDate", "tag_category": "tagCategory", "score": "averageScore", "min_tag_percent": "minimumTagRank", "country": "countryOfOrigin", @@ -24,16 +24,10 @@ no_mod_searches = ["search", "season", "year", "adult", "min_tag_percent", "limit", "sort_by", "source", "country"] searches = mod_searches + no_mod_searches sort_options = {"score": "SCORE_DESC", "popular": "POPULARITY_DESC", "trending": "TRENDING_DESC"} -userlist_sort_options = ["score", "last_updated", "title", "start_date"] -userlist_sort_translation = { - "score": "SCORE", "last_updated": "UPDATED_TIME", - "title": "MEDIA_TITLE_NATIVE", "start_date": "STARTED_ON" -} -userlist_status = ["watching", "completed", "paused", "dropped", "planning"] -userlist_status_translation = { - "watching": "CURRENT", "completed": "COMPLETED", - "paused": "PAUSED", "dropped" :"DROPPED", - "planning": "PLANNING" +userlist_sort_options = { + "score": "SCORE_DESC", "status": "STATUS_DESC", "progress": "PROGRESS_DESC", + "last_updated": "UPDATED_TIME_DESC", "last_added": "ADDED_TIME_DESC", "start_date": "STARTED_ON_DESC", + "completed_date": "FINISHED_ON_DESC", "popularity": "MEDIA_POPULARITY_DESC" } media_season = {"winter": "WINTER", "spring": "SPRING", "summer": "SUMMER", "fall": "FALL"} media_format = {"tv": "TV", "short": "TV_SHORT", "movie": "MOVIE", "special": "SPECIAL", "ova": "OVA", "ona": "ONA", "music": "MUSIC"} @@ -225,16 +219,43 @@ def _relations(self, anilist_id, ignore_ids=None): return anilist_ids, ignore_ids, name - def _userlist(self, username, status, sort_by): - query = "query ($userName: String, $status: MediaListStatus) {MediaListCollection (userName: $userName, status: $status, type: ANIME) {lists {status entries {media{id title{romaji english}}}}}}" - variables = {"userName": username, "status": status, "sort": sort_by} + def _userlist(self, username, list_name, sort_by): + query = """ + query ($user: String, $sort: MediaListStatus) { + MediaListCollection (userName: $user, sort: $sort, type: ANIME) { + lists { + name + entries { + media{id} + } + } + } + } + """ + variables = {"user": username, "sort": userlist_sort_options[sort_by]} + for alist in self._request(query, variables)["data"]["MediaListCollection"]["lists"]: + if alist["name"] == list_name: + return [m["media"]["id"] for m in alist["entries"]] + return [] + + def validate_userlist(self, data): + query = """ + query ($user: String) { + MediaListCollection (userName: $user, type: ANIME) { + lists {name} + } + } + """ + variables = {"user": data["username"]} json_obj = self._request(query, variables) - lists = json_obj['data']['MediaListCollection']['lists'] - anilist_ids = [] - for list in lists: - for media in list['entries']: - anilist_ids.append(media['media']['id']) - return anilist_ids + if not json_obj["data"]["MediaListCollection"]: + raise Failed(f"AniList Error: User: {data['username']} not found") + list_names = [n["name"] for n in json_obj["data"]["MediaListCollection"]["lists"]] + if not list_names: + raise Failed(f"AniList Error: User: {data['username']} has no Lists") + if data["list_name"] in list_names: + return data + raise Failed(f"AniList Error: List: {data['list_name']} not found\nOptions: {', '.join(list_names)}") def validate(self, name, data): valid = [] @@ -270,8 +291,8 @@ def get_anilist_ids(self, method, data): anilist_ids, _, name = self._relations(data) logger.info(f"Processing AniList Relations: ({data}) {name} ({len(anilist_ids)} Anime)") elif method == "anilist_userlist": - anilist_ids = self._userlist(data["username"], data["status"], data['sort_by']) - logger.info(f"Processing AniList Userlist: Anime from {data['username']}'s {pretty_names[data['status']]} list sorted by {pretty_names[data['sort_by']]}") + anilist_ids = self._userlist(data["username"], data["list_name"], data["sort_by"]) + logger.info(f"Processing AniList Userlist: {data['list_name']} from {data['username']} sorted by {pretty_user[data['sort_by']]}") else: if method == "anilist_popular": data = {"limit": data, "popularity.gt": 3, "sort_by": "popular"} diff --git a/modules/builder.py b/modules/builder.py index 8c19c79f2..129563d09 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -958,11 +958,11 @@ def _anilist(self, method_name, method_data): elif method_name == "anilist_userlist": for dict_data in util.parse(self.Type, method_name, method_data, datatype="listdict"): dict_methods = {dm.lower(): dm for dm in dict_data} - self.builders.append((method_name, { + self.builders.append((method_name, self.config.AniList.validate_userlist({ "username": util.parse(self.Type, "username", dict_data, methods=dict_methods, parent=method_name), - "status": util.parse(self.Type, "status", dict_data, methods=dict_methods, parent=method_name, default="watching", options=anilist.userlist_status, translation=anilist.userlist_status_translation), - "sort_by": util.parse(self.Type, "sort_by", dict_data, methods=dict_methods, parent=method_name, default="score", options=anilist.userlist_sort_options, translation=anilist.userlist_sort_translation), - })) + "list_name": util.parse(self.Type, "list_name", dict_data, methods=dict_methods, parent=method_name), + "sort_by": util.parse(self.Type, "sort_by", dict_data, methods=dict_methods, parent=method_name, default="score", options=anilist.userlist_sort_options), + }))) elif method_name == "anilist_search": if self.current_time.month in [12, 1, 2]: current_season = "winter" elif self.current_time.month in [3, 4, 5]: current_season = "spring" From 6e3571a0de9d18fe0f5b3ef58078143a4896a732 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Sun, 13 Mar 2022 23:39:47 -0400 Subject: [PATCH 13/32] [11] fix tvdb genres --- VERSION | 2 +- modules/tvdb.py | 16 ++++++++-------- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/VERSION b/VERSION index bdf223dec..f0163470e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop10 +1.16.1-develop11 diff --git a/modules/tvdb.py b/modules/tvdb.py index 54a1cf219..e82db9a4e 100644 --- a/modules/tvdb.py +++ b/modules/tvdb.py @@ -66,11 +66,11 @@ def __init__(self, tvdb_url, language, is_movie, config): else: raise Failed(f"TVDb Error: Could not find a TVDb {self.media_type} ID at the URL {self.tvdb_url}") - def parse_page(xpath): + def parse_page(xpath, is_list=False): parse_results = response.xpath(xpath) if len(parse_results) > 0: parse_results = [r.strip() for r in parse_results if len(r) > 0] - return parse_results[0] if len(parse_results) > 0 else None + return parse_results if is_list else parse_results[0] if len(parse_results) > 0 else None def parse_title_summary(lang=None): place = "//div[@class='change_translation_text' and " @@ -85,15 +85,15 @@ def parse_title_summary(lang=None): if not self.title: raise Failed(f"TVDb Error: Name not found from TVDb URL: {self.tvdb_url}") - self.poster_path = parse_page("//div[@class='row hidden-xs hidden-sm']/div/img/@src") + self.poster_path = parse_page("(//h2[@class='mt-4' and text()='Posters']/following::div/a/@href)[1]") self.background_path = parse_page("(//h2[@class='mt-4' and text()='Backgrounds']/following::div/a/@href)[1]") if self.is_movie: - self.directors = parse_page("//strong[text()='Directors']/parent::li/span/a/text()[normalize-space()]") - self.writers = parse_page("//strong[text()='Writers']/parent::li/span/a/text()[normalize-space()]") - self.studios = parse_page("//strong[text()='Studio']/parent::li/span/a/text()[normalize-space()]") + self.directors = parse_page("//strong[text()='Directors']/parent::li/span/a/text()[normalize-space()]", is_list=True) + self.writers = parse_page("//strong[text()='Writers']/parent::li/span/a/text()[normalize-space()]", is_list=True) + self.studios = parse_page("//strong[text()='Studio']/parent::li/span/a/text()[normalize-space()]", is_list=True) else: - self.networks = parse_page("//strong[text()='Networks']/parent::li/span/a/text()[normalize-space()]") - self.genres = parse_page("//strong[text()='Genres']/parent::li/span/a/text()[normalize-space()]") + self.networks = parse_page("//strong[text()='Networks']/parent::li/span/a/text()[normalize-space()]", is_list=True) + self.genres = parse_page("//strong[text()='Genres']/parent::li/span/a/text()[normalize-space()]", is_list=True) tmdb_id = None imdb_id = None From 24beeb8c4de7a9cbec85e12bbd17b74e5ce971ee Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Mon, 14 Mar 2022 00:53:07 -0400 Subject: [PATCH 14/32] [12] #750 add mass_originally_available_update --- VERSION | 2 +- modules/cache.py | 38 +++++++++++++++------------ modules/config.py | 26 ++++++++----------- modules/library.py | 3 ++- modules/mdblist.py | 5 ++++ modules/omdb.py | 62 ++++++++++++++++++++------------------------ modules/tvdb.py | 3 +++ plex_meta_manager.py | 30 ++++++++++++++++++--- 8 files changed, 98 insertions(+), 71 deletions(-) diff --git a/VERSION b/VERSION index f0163470e..63eb2b7c2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop11 +1.16.1-develop12 diff --git a/modules/cache.py b/modules/cache.py index 6666e828d..07290cd80 100644 --- a/modules/cache.py +++ b/modules/cache.py @@ -22,7 +22,9 @@ def __init__(self, config_path, expiration): cursor.execute("DROP TABLE IF EXISTS imdb_to_tvdb_map") cursor.execute("DROP TABLE IF EXISTS tmdb_to_tvdb_map") cursor.execute("DROP TABLE IF EXISTS imdb_map") + cursor.execute("DROP TABLE IF EXISTS mdb_data") cursor.execute("DROP TABLE IF EXISTS omdb_data") + cursor.execute("DROP TABLE IF EXISTS omdb_data2") cursor.execute( """CREATE TABLE IF NOT EXISTS guids_map ( key INTEGER PRIMARY KEY, @@ -70,11 +72,12 @@ def __init__(self, config_path, expiration): expiration_date TEXT)""" ) cursor.execute( - """CREATE TABLE IF NOT EXISTS omdb_data2 ( + """CREATE TABLE IF NOT EXISTS omdb_data3 ( key INTEGER PRIMARY KEY, imdb_id TEXT UNIQUE, title TEXT, year INTEGER, + released TEXT, content_rating TEXT, genres TEXT, imdb_rating REAL, @@ -87,11 +90,12 @@ def __init__(self, config_path, expiration): expiration_date TEXT)""" ) cursor.execute( - """CREATE TABLE IF NOT EXISTS mdb_data ( + """CREATE TABLE IF NOT EXISTS mdb_data2 ( key INTEGER PRIMARY KEY, key_id TEXT UNIQUE, title TEXT, year INTEGER, + released TEXT, type TEXT, imdbid TEXT, traktid INTEGER, @@ -326,12 +330,13 @@ def query_omdb(self, imdb_id, expiration): with sqlite3.connect(self.cache_path) as connection: connection.row_factory = sqlite3.Row with closing(connection.cursor()) as cursor: - cursor.execute("SELECT * FROM omdb_data2 WHERE imdb_id = ?", (imdb_id,)) + cursor.execute("SELECT * FROM omdb_data3 WHERE imdb_id = ?", (imdb_id,)) row = cursor.fetchone() if row: omdb_dict["imdbID"] = row["imdb_id"] if row["imdb_id"] else None omdb_dict["Title"] = row["title"] if row["title"] else None omdb_dict["Year"] = row["year"] if row["year"] else None + omdb_dict["Released"] = row["released"] if row["released"] else None omdb_dict["Rated"] = row["content_rating"] if row["content_rating"] else None omdb_dict["Genre"] = row["genres"] if row["genres"] else None omdb_dict["imdbRating"] = row["imdb_rating"] if row["imdb_rating"] else None @@ -352,14 +357,14 @@ def update_omdb(self, expired, omdb, expiration): with sqlite3.connect(self.cache_path) as connection: connection.row_factory = sqlite3.Row with closing(connection.cursor()) as cursor: - cursor.execute("INSERT OR IGNORE INTO omdb_data2(imdb_id) VALUES(?)", (omdb.imdb_id,)) - update_sql = "UPDATE omdb_data2 SET title = ?, year = ?, content_rating = ?, genres = ?, " \ + cursor.execute("INSERT OR IGNORE INTO omdb_data3(imdb_id) VALUES(?)", (omdb.imdb_id,)) + update_sql = "UPDATE omdb_data3 SET title = ?, year = ?, released = ?, content_rating = ?, genres = ?, " \ "imdb_rating = ?, imdb_votes = ?, metacritic_rating = ?, type = ?, series_id = ?, " \ "season_num = ?, episode_num = ?, expiration_date = ? WHERE imdb_id = ?" - cursor.execute(update_sql, (omdb.title, omdb.year, omdb.content_rating, omdb.genres_str, - omdb.imdb_rating, omdb.imdb_votes, omdb.metacritic_rating, omdb.type, - omdb.series_id, omdb.season_num, omdb.episode_num, - expiration_date.strftime("%Y-%m-%d"), omdb.imdb_id)) + cursor.execute(update_sql, ( + omdb.title, omdb.year, omdb.released.strftime("%d %b %Y"), omdb.content_rating, omdb.genres_str, + omdb.imdb_rating, omdb.imdb_votes, omdb.metacritic_rating, omdb.type, omdb.series_id, omdb.season_num, + omdb.episode_num, expiration_date.strftime("%Y-%m-%d"), omdb.imdb_id)) def query_mdb(self, key_id, expiration): mdb_dict = {} @@ -367,11 +372,12 @@ def query_mdb(self, key_id, expiration): with sqlite3.connect(self.cache_path) as connection: connection.row_factory = sqlite3.Row with closing(connection.cursor()) as cursor: - cursor.execute("SELECT * FROM mdb_data WHERE key_id = ?", (key_id,)) + cursor.execute("SELECT * FROM mdb_data2 WHERE key_id = ?", (key_id,)) row = cursor.fetchone() if row: mdb_dict["title"] = row["title"] if row["title"] else None mdb_dict["year"] = row["year"] if row["year"] else None + mdb_dict["released"] = row["released"] if row["released"] else None mdb_dict["type"] = row["type"] if row["type"] else None mdb_dict["imdbid"] = row["imdbid"] if row["imdbid"] else None mdb_dict["traktid"] = row["traktid"] if row["traktid"] else None @@ -399,16 +405,16 @@ def update_mdb(self, expired, key_id, mdb, expiration): with sqlite3.connect(self.cache_path) as connection: connection.row_factory = sqlite3.Row with closing(connection.cursor()) as cursor: - cursor.execute("INSERT OR IGNORE INTO mdb_data(key_id) VALUES(?)", (key_id,)) - update_sql = "UPDATE mdb_data SET title = ?, year = ?, type = ?, imdbid = ?, traktid = ?, " \ + cursor.execute("INSERT OR IGNORE INTO mdb_data2(key_id) VALUES(?)", (key_id,)) + update_sql = "UPDATE mdb_data2 SET title = ?, year = ?, released = ?, type = ?, imdbid = ?, traktid = ?, " \ "tmdbid = ?, score = ?, imdb_rating = ?, metacritic_rating = ?, metacriticuser_rating = ?, " \ "trakt_rating = ?, tomatoes_rating = ?, tomatoesaudience_rating = ?, tmdb_rating = ?, " \ "letterboxd_rating = ?, certification = ?, commonsense = ?, expiration_date = ? WHERE key_id = ?" cursor.execute(update_sql, ( - mdb.title, mdb.year, mdb.type, mdb.imdbid, mdb.traktid, mdb.tmdbid, mdb.score, mdb.imdb_rating, - mdb.metacritic_rating, mdb.metacriticuser_rating, mdb.trakt_rating, mdb.tomatoes_rating, - mdb.tomatoesaudience_rating, mdb.tmdb_rating, mdb.letterboxd_rating, mdb.content_rating, - mdb.commonsense, expiration_date.strftime("%Y-%m-%d"), key_id + mdb.title, mdb.year, mdb.released.strftime("%Y-%m-%d"), mdb.type, mdb.imdbid, mdb.traktid, mdb.tmdbid, + mdb.score, mdb.imdb_rating, mdb.metacritic_rating, mdb.metacriticuser_rating, mdb.trakt_rating, + mdb.tomatoes_rating, mdb.tomatoesaudience_rating, mdb.tmdb_rating, mdb.letterboxd_rating, + mdb.content_rating, mdb.commonsense, expiration_date.strftime("%Y-%m-%d"), key_id )) def query_tmdb_movie(self, tmdb_id, expiration): diff --git a/modules/config.py b/modules/config.py index 9b8a0f4dc..b1037ae71 100644 --- a/modules/config.py +++ b/modules/config.py @@ -33,6 +33,7 @@ sync_modes = {"append": "Only Add Items to the Collection or Playlist", "sync": "Add & Remove Items from the Collection or Playlist"} mass_genre_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb", "tvdb": "Use TVDb Metadata"} mass_content_options = {"omdb": "Use IMDb Metadata through OMDb", "mdb": "Use MdbList Metadata", "mdb_commonsense": "Use Commonsense Rating through MDbList"} +mass_available_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb", "mdb": "Use MdbList Metadata", "tvdb": "Use TVDb Metadata"} mass_rating_options = { "tmdb": "Use TMDb Rating", "omdb": "Use IMDb Rating through OMDb", @@ -600,7 +601,8 @@ def check_dict(attr): "metadata_backup": None, "genre_collections": None, "update_blank_track_titles": None, - "mass_content_rating_update": None + "mass_content_rating_update": None, + "mass_originally_available_update": None } display_name = f"{params['name']} ({params['mapping_name']})" if lib and "library_name" in lib and lib["library_name"] else params["mapping_name"] @@ -669,6 +671,8 @@ def check_dict(attr): params["mass_critic_rating_update"] = check_for_attribute(lib["operations"], "mass_critic_rating_update", test_list=mass_rating_options, default_is_none=True, save=False) if "mass_content_rating_update" in lib["operations"]: params["mass_content_rating_update"] = check_for_attribute(lib["operations"], "mass_content_rating_update", test_list=mass_content_options, default_is_none=True, save=False) + if "mass_originally_available_update" in lib["operations"]: + params["mass_originally_available_update"] = check_for_attribute(lib["operations"], "mass_originally_available_update", test_list=mass_available_options, default_is_none=True, save=False) if "mass_trakt_rating_update" in lib["operations"]: params["mass_trakt_rating_update"] = check_for_attribute(lib["operations"], "mass_trakt_rating_update", var_type="bool", default=False, save=False) if "split_duplicates" in lib["operations"]: @@ -785,20 +789,12 @@ def error_check(attr, service): self.errors.append(err) logger.error(err) - if self.OMDb is None and params["mass_genre_update"] == "omdb": - error_check("mass_genre_update", "OMDb") - if self.OMDb is None and params["mass_audience_rating_update"] == "omdb": - error_check("mass_audience_rating_update", "OMDb") - if self.OMDb is None and params["mass_critic_rating_update"] == "omdb": - error_check("mass_critic_rating_update", "OMDb") - if self.OMDb is None and params["mass_content_rating_update"] == "omdb": - error_check("mass_content_rating_update", "OMDb") - if not self.Mdblist.has_key and params["mass_audience_rating_update"] in util.mdb_types: - error_check("mass_audience_rating_update", "MdbList API") - if not self.Mdblist.has_key and params["mass_critic_rating_update"] in util.mdb_types: - error_check("mass_critic_rating_update", "MdbList API") - if not self.Mdblist.has_key and params["mass_content_rating_update"] in ["mdb", "mdb_commonsense"]: - error_check("mass_content_rating_update", "MdbList API") + for mass_key in ["mass_genre_update", "mass_audience_rating_update", "mass_critic_rating_update", "mass_content_rating_update", "mass_originally_available_update"]: + if params[mass_key] == "omdb" and self.OMDb is None: + error_check(mass_key, "OMDb") + if params[mass_key].startswith("mdb") and not self.Mdblist.has_key: + error_check(mass_key, "MdbList API") + if self.Trakt is None and params["mass_trakt_rating_update"]: error_check("mass_trakt_rating_update", "Trakt") diff --git a/modules/library.py b/modules/library.py index 6e57f27c6..d0c41f425 100644 --- a/modules/library.py +++ b/modules/library.py @@ -72,6 +72,7 @@ def __init__(self, config, params): self.mass_audience_rating_update = params["mass_audience_rating_update"] self.mass_critic_rating_update = params["mass_critic_rating_update"] self.mass_content_rating_update = params["mass_content_rating_update"] + self.mass_originally_available_update = params["mass_originally_available_update"] self.mass_trakt_rating_update = params["mass_trakt_rating_update"] self.radarr_add_all_existing = params["radarr_add_all_existing"] self.radarr_remove_by_tag = params["radarr_remove_by_tag"] @@ -94,7 +95,7 @@ def __init__(self, config, params): self.status = {} self.items_library_operation = True if self.assets_for_all or self.mass_genre_update or self.mass_audience_rating_update \ - or self.mass_critic_rating_update or self.mass_content_rating_update or self.mass_trakt_rating_update \ + or self.mass_critic_rating_update or self.mass_content_rating_update or self.mass_originally_available_update or self.mass_trakt_rating_update \ or self.genre_mapper or self.content_rating_mapper or self.tmdb_collections or self.radarr_add_all_existing or self.sonarr_add_all_existing else False self.library_operation = True if self.items_library_operation or self.delete_unmanaged_collections or self.delete_collections_with_less \ or self.radarr_remove_by_tag or self.sonarr_remove_by_tag or self.mass_collection_mode \ diff --git a/modules/mdblist.py b/modules/mdblist.py index a13330366..23a134606 100644 --- a/modules/mdblist.py +++ b/modules/mdblist.py @@ -1,3 +1,4 @@ +from datetime import datetime from modules import util from modules.util import Failed from urllib.parse import urlparse @@ -17,6 +18,10 @@ def __init__(self, data): self._data = data self.title = data["title"] self.year = util.check_num(data["year"]) + try: + self.released = datetime.strptime(data["released"], "%Y-%m-%d") + except ValueError: + self.released = None self.type = data["type"] self.imdbid = data["imdbid"] self.traktid = util.check_num(data["traktid"]) diff --git a/modules/omdb.py b/modules/omdb.py index 1cf954cc8..7d7a0ca70 100644 --- a/modules/omdb.py +++ b/modules/omdb.py @@ -1,3 +1,4 @@ +from datetime import datetime from modules import util from modules.util import Failed @@ -11,40 +12,33 @@ def __init__(self, imdb_id, data): self._data = data if data["Response"] == "False": raise Failed(f"OMDb Error: {data['Error']} IMDb ID: {imdb_id}") - self.title = data["Title"] - try: - self.year = int(data["Year"]) - except (ValueError, TypeError): - self.year = None - self.content_rating = data["Rated"] - self.genres = util.get_list(data["Genre"]) - self.genres_str = data["Genre"] - try: - self.imdb_rating = float(data["imdbRating"]) - except (ValueError, TypeError): - self.imdb_rating = None - try: - self.imdb_votes = int(str(data["imdbVotes"]).replace(',', '')) - except (ValueError, TypeError): - self.imdb_votes = None - try: - self.metacritic_rating = int(data["Metascore"]) - except (ValueError, TypeError): - self.metacritic_rating = None - self.imdb_id = data["imdbID"] - self.type = data["Type"] - try: - self.series_id = data["seriesID"] - except (ValueError, TypeError, KeyError): - self.series_id = None - try: - self.season_num = int(data["Season"]) - except (ValueError, TypeError, KeyError): - self.season_num = None - try: - self.episode_num = int(data["Episode"]) - except (ValueError, TypeError, KeyError): - self.episode_num = None + def _parse(key, is_int=False, is_float=False, is_date=False, replace=None): + try: + value = str(data[key]).replace(replace, '') if replace else data[key] + if is_int: + return int(value) + elif is_float: + return float(value) + elif is_date: + return datetime.strptime(value, "%d %b %Y") + else: + return value + except (ValueError, TypeError, KeyError): + return None + self.title = _parse("Title") + self.year = _parse("Year", is_int=True) + self.released = _parse("Released", is_date=True) + self.content_rating = _parse("Rated") + self.genres_str = _parse("Genre") + self.genres = util.get_list(self.genres_str) + self.imdb_rating = _parse("imdbRating", is_float=True) + self.imdb_votes = _parse("imdbVotes", is_int=True, replace=",") + self.metacritic_rating = _parse("Metascore", is_int=True) + self.imdb_id = _parse("imdbID") + self.type = _parse("Type") + self.series_id = _parse("seriesID") + self.season_num = _parse("Season", is_int=True) + self.episode_num = _parse("Episode", is_int=True) class OMDb: diff --git a/modules/tvdb.py b/modules/tvdb.py index e82db9a4e..bde31947a 100644 --- a/modules/tvdb.py +++ b/modules/tvdb.py @@ -1,4 +1,5 @@ import requests, time +from datetime import datetime from lxml.etree import ParserError from modules import util from modules.util import Failed @@ -91,8 +92,10 @@ def parse_title_summary(lang=None): self.directors = parse_page("//strong[text()='Directors']/parent::li/span/a/text()[normalize-space()]", is_list=True) self.writers = parse_page("//strong[text()='Writers']/parent::li/span/a/text()[normalize-space()]", is_list=True) self.studios = parse_page("//strong[text()='Studio']/parent::li/span/a/text()[normalize-space()]", is_list=True) + self.released = datetime.strptime(parse_page("//strong[text()='Released']/parent::li/span/text()[normalize-space()]"), "%B %d, %Y") else: self.networks = parse_page("//strong[text()='Networks']/parent::li/span/a/text()[normalize-space()]", is_list=True) + self.released = datetime.strptime(parse_page("//strong[text()='First Aired']/parent::li/span/text()[normalize-space()]"), "%B %d, %Y") self.genres = parse_page("//strong[text()='Genres']/parent::li/span/a/text()[normalize-space()]", is_list=True) tmdb_id = None diff --git a/plex_meta_manager.py b/plex_meta_manager.py index a835d4187..a1ce95bda 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -407,6 +407,7 @@ def library_operations(config, library): logger.debug(f"Mass Audience Rating Update: {library.mass_audience_rating_update}") logger.debug(f"Mass Critic Rating Update: {library.mass_critic_rating_update}") logger.debug(f"Mass Content Rating Update: {library.mass_content_rating_update}") + logger.debug(f"Mass Originally Available Update: {library.mass_originally_available_update}") logger.debug(f"Mass Trakt Rating Update: {library.mass_trakt_rating_update}") logger.debug(f"Mass Collection Mode Update: {library.mass_collection_mode}") logger.debug(f"Split Duplicates: {library.split_duplicates}") @@ -483,12 +484,14 @@ def library_operations(config, library): sonarr_adds.append((tvdb_id, path)) tmdb_item = None - if library.tmdb_collections or library.mass_genre_update == "tmdb" or library.mass_audience_rating_update == "tmdb" or library.mass_critic_rating_update == "tmdb": + if library.tmdb_collections or library.mass_genre_update == "tmdb" or library.mass_audience_rating_update == "tmdb" \ + or library.mass_critic_rating_update == "tmdb" or library.mass_originally_available_update == "tmdb": tmdb_item = config.TMDb.get_item(item, tmdb_id, tvdb_id, imdb_id, is_movie=library.is_movie) omdb_item = None if library.mass_genre_update == "omdb" or library.mass_audience_rating_update == "omdb" \ - or library.mass_critic_rating_update == "omdb" or library.mass_content_rating_update == "omdb": + or library.mass_critic_rating_update == "omdb" or library.mass_content_rating_update == "omdb" \ + or library.mass_originally_available_update == "omdb": if config.OMDb.limit is False: if tmdb_id and not imdb_id: imdb_id = config.Convert.tmdb_to_imdb(tmdb_id) @@ -506,7 +509,7 @@ def library_operations(config, library): logger.info(f"{item.title[:25]:<25} | No IMDb ID for Guid: {item.guid}") tvdb_item = None - if library.mass_genre_update == "tvdb": + if library.mass_genre_update == "tvdb" or library.mass_originally_available_update == "tvdb": if tvdb_id: try: tvdb_item = config.TVDb.get_item(tvdb_id, library.is_movie) @@ -517,7 +520,7 @@ def library_operations(config, library): mdb_item = None if library.mass_audience_rating_update in util.mdb_types or library.mass_critic_rating_update in util.mdb_types \ - or library.mass_content_rating_update in ["mdb", "mdb_commonsense"]: + or library.mass_content_rating_update in ["mdb", "mdb_commonsense"] or library.mass_originally_available_update == "mdb": if config.Mdblist.limit is False: if tmdb_id and not imdb_id: imdb_id = config.Convert.tmdb_to_imdb(tmdb_id) @@ -613,6 +616,25 @@ def get_rating(attribute): logger.info(f"{item.title[:25]:<25} | Content Rating | {new_rating}") except Failed: pass + if library.mass_originally_available_update: + try: + if omdb_item and library.mass_originally_available_update == "omdb": + new_date = omdb_item.released + elif mdb_item and library.mass_originally_available_update == "mdb": + new_date = mdb_item.released + elif tvdb_item and library.mass_content_rating_update == "tvdb": + new_date = tvdb_item.released + elif tmdb_item and library.mass_content_rating_update == "tvdb": + new_date = tmdb_item.release_date if library.is_movie else tmdb_item.first_air_date + else: + raise Failed + if new_date is None: + logger.info(f"{item.title[:25]:<25} | No Originally Available Date Found") + elif str(item.rating) != str(new_date): + library.edit_query(item, {"originallyAvailableAt.value": new_date.strftime("%Y-%m-%d"), "originallyAvailableAt.locked": 1}) + logger.info(f"{item.title[:25]:<25} | Originally Available Date | {new_date.strftime('%Y-%m-%d')}") + except Failed: + pass if library.genre_mapper or library.content_rating_mapper: try: From 569826087d5ed0e28d8d26a892435ad64cdbefcd Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Mon, 14 Mar 2022 08:52:55 -0400 Subject: [PATCH 15/32] [13] fix list index error --- VERSION | 2 +- modules/tmdb.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index 63eb2b7c2..6ffda7fc1 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop12 +1.16.1-develop13 diff --git a/modules/tmdb.py b/modules/tmdb.py index 2544fc766..9be07e74f 100644 --- a/modules/tmdb.py +++ b/modules/tmdb.py @@ -110,7 +110,7 @@ def __init__(self, tmdb, tmdb_id, ignore_cache=False): self.original_title = data["original_title"] if isinstance(data, dict) else data.original_title self.release_date = data["release_date"] if isinstance(data, dict) else data.release_date - self.studio = data["studio"] if isinstance(data, dict) else data.companies[0].name + self.studio = data["studio"] if isinstance(data, dict) else data.companies[0].name if data.companies else None self.collection_id = data["collection_id"] if isinstance(data, dict) else data.collection.id if data.collection else None self.collection_name = data["collection_name"] if isinstance(data, dict) else data.collection.name if data.collection else None @@ -134,7 +134,7 @@ def __init__(self, tmdb, tmdb_id, ignore_cache=False): self.last_air_date = data["last_air_date"] if isinstance(data, dict) else data.last_air_date self.status = data["status"] if isinstance(data, dict) else data.status self.type = data["type"] if isinstance(data, dict) else data.type - self.studio = data["studio"] if isinstance(data, dict) else data.networks[0].name + self.studio = data["studio"] if isinstance(data, dict) else data.networks[0].name if data.networks else None self.tvdb_id = data["tvdb_id"] if isinstance(data, dict) else data.tvdb_id loop = data["countries"].split("|") if isinstance(data, dict) else data.origin_countries self.countries = [TMDbCountry(c) for c in loop] From 0f96c51946774ed65ac6bcc91ef9578d162fda40 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Mon, 14 Mar 2022 09:02:02 -0400 Subject: [PATCH 16/32] [14] fix NoneType Error --- VERSION | 2 +- modules/config.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 6ffda7fc1..ecdaacb8f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop13 +1.16.1-develop14 diff --git a/modules/config.py b/modules/config.py index b1037ae71..4289b1486 100644 --- a/modules/config.py +++ b/modules/config.py @@ -792,7 +792,7 @@ def error_check(attr, service): for mass_key in ["mass_genre_update", "mass_audience_rating_update", "mass_critic_rating_update", "mass_content_rating_update", "mass_originally_available_update"]: if params[mass_key] == "omdb" and self.OMDb is None: error_check(mass_key, "OMDb") - if params[mass_key].startswith("mdb") and not self.Mdblist.has_key: + if params[mass_key] and params[mass_key].startswith("mdb") and not self.Mdblist.has_key: error_check(mass_key, "MdbList API") if self.Trakt is None and params["mass_trakt_rating_update"]: From 9239888b711f23c5fc6fd80999f78b248e22d428 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Mon, 14 Mar 2022 09:29:34 -0400 Subject: [PATCH 17/32] [15] minimum_items checks smart collections --- VERSION | 2 +- plex_meta_manager.py | 22 +++++++++++++--------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/VERSION b/VERSION index ecdaacb8f..8858c6e4c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop14 +1.16.1-develop15 diff --git a/plex_meta_manager.py b/plex_meta_manager.py index a1ce95bda..7cdfca876 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -854,7 +854,6 @@ def run_collection(config, library, metadata, requested_collections): items_added = 0 items_removed = 0 - valid = True if not builder.smart_url and builder.builders and not builder.blank_collection: logger.info("") logger.info(f"Sync Mode: {'sync' if builder.sync else 'append'}") @@ -883,14 +882,6 @@ def run_collection(config, library, metadata, requested_collections): items_removed = builder.sync_collection() library.stats["removed"] += items_removed library.status[mapping_name]["removed"] = items_removed - elif len(builder.added_items) + builder.beginning_count < builder.minimum and builder.build_collection: - logger.info("") - logger.info(f"Collection Minimum: {builder.minimum} not met for {mapping_name} Collection") - valid = False - if builder.details["delete_below_minimum"] and builder.obj: - logger.info("") - logger.info(builder.delete()) - builder.deleted = True if builder.do_missing and (len(builder.missing_movies) > 0 or len(builder.missing_shows) > 0): radarr_add, sonarr_add = builder.run_missing() @@ -899,6 +890,19 @@ def run_collection(config, library, metadata, requested_collections): library.stats["sonarr"] += sonarr_add library.status[mapping_name]["sonarr"] += sonarr_add + valid = True + if builder.build_collection and ( + (builder.smart_url and len(library.get_filter_items(builder.smart_url)) < builder.minimum) + or (len(builder.added_items) + builder.beginning_count < builder.minimum) + ): + logger.info("") + logger.info(f"Collection Minimum: {builder.minimum} not met for {mapping_name} Collection") + valid = False + if builder.details["delete_below_minimum"] and builder.obj: + logger.info("") + logger.info(builder.delete()) + builder.deleted = True + run_item_details = True if valid and builder.build_collection and (builder.builders or builder.smart_url or builder.blank_collection): try: From ed5358e9c80a2d17d604086b889089e8ed1e5f6a Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Mon, 14 Mar 2022 09:48:42 -0400 Subject: [PATCH 18/32] [16] #774 Add key and key_name to template --- VERSION | 2 +- modules/meta.py | 33 ++++++++++++++++++++------------- 2 files changed, 21 insertions(+), 14 deletions(-) diff --git a/VERSION b/VERSION index 8858c6e4c..d53c5adb0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop15 +1.16.1-develop16 diff --git a/modules/meta.py b/modules/meta.py index 3c87b648a..9fc211329 100644 --- a/modules/meta.py +++ b/modules/meta.py @@ -246,9 +246,11 @@ def __init__(self, config, library, file_type, path): all_items = None if self.dynamic_collections: logger.info("") - logger.separator(f"Dynamic Collections") - logger.info("") + logger.separator("Dynamic Collections") for map_name, dynamic in self.dynamic_collections.items(): + logger.info("") + logger.separator(f"Building {map_name} Dynamic Collections", space=False, border=False) + logger.info("") try: methods = {dm.lower(): dm for dm in dynamic} if "type" not in methods: @@ -445,23 +447,28 @@ def _check_dict(check_dict): if key not in exclude: other_keys.append(key) continue - template_call = {"name": template_name, auto_type: [key] + addons[key] if key in addons else key} + if key in key_name_override: + key_name = key_name_override[key] + else: + key_name = value + for prefix in remove_prefix: + if key_name.startswith(prefix): + key_name = key_name[len(prefix):].strip() + for suffix in remove_suffix: + if key_name.endswith(suffix): + key_name = key_name[:-len(suffix)].strip() + template_call = { + "name": template_name, + auto_type: [key] + addons[key] if key in addons else key, + "key_name": key_name, "key": key + } for k, v in template_variables.items(): if key in v: template_call[k] = v[key] if key in title_override: collection_title = title_override[key] else: - if key in key_name_override: - value = key_name_override[key] - else: - for prefix in remove_prefix: - if value.startswith(prefix): - value = value[len(prefix):].strip() - for suffix in remove_suffix: - if value.endswith(suffix): - value = value[:-len(suffix)].strip() - collection_title = title_format.replace("<>", value).replace("<<key_name>>", value) + collection_title = title_format.replace("<<title>>", key_name).replace("<<key_name>>", key_name) if collection_title in col_names: logger.warning(f"Config Warning: Skipping duplicate collection: {collection_title}") else: From c10b8916c9c12ed0460b8ba7608cf83b27c63df9 Mon Sep 17 00:00:00 2001 From: meisnate12 <meisnate12@gmail.com> Date: Mon, 14 Mar 2022 10:00:55 -0400 Subject: [PATCH 19/32] [17] #776 all key values are lists --- VERSION | 2 +- modules/meta.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index d53c5adb0..202e6f97d 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop16 +1.16.1-develop17 diff --git a/modules/meta.py b/modules/meta.py index 9fc211329..5060a33e9 100644 --- a/modules/meta.py +++ b/modules/meta.py @@ -459,7 +459,7 @@ def _check_dict(check_dict): key_name = key_name[:-len(suffix)].strip() template_call = { "name": template_name, - auto_type: [key] + addons[key] if key in addons else key, + auto_type: [key] + addons[key] if key in addons else [key], "key_name": key_name, "key": key } for k, v in template_variables.items(): From db16044dae3e09aca0b5ddb58a23d9e38181aa5b Mon Sep 17 00:00:00 2001 From: meisnate12 <meisnate12@gmail.com> Date: Mon, 14 Mar 2022 11:05:22 -0400 Subject: [PATCH 20/32] [18] fix cache storage --- VERSION | 2 +- modules/cache.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 202e6f97d..98c90fdcc 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop17 +1.16.1-develop18 diff --git a/modules/cache.py b/modules/cache.py index 07290cd80..82983b50b 100644 --- a/modules/cache.py +++ b/modules/cache.py @@ -513,7 +513,7 @@ def update_tmdb_show(self, expired, obj, expiration): obj.vote_count, obj.vote_average, obj.language_iso, obj.language_name, "|".join(obj.genres), "|".join(obj.keywords), obj.first_air_date.strftime("%Y-%m-%d") if obj.first_air_date else None, obj.last_air_date.strftime("%Y-%m-%d") if obj.last_air_date else None, - obj.status, obj.type, obj.tvdb_id, "|".join(obj.countries), "|".join(obj.seasons), + obj.status, obj.type, obj.tvdb_id, "|".join([str(c) for c in obj.countries]), "|".join([str(s) for s in obj.seasons]), expiration_date.strftime("%Y-%m-%d"), obj.tmdb_id )) From 88903e0c37a19bb06590378cdc77e22845a2ef17 Mon Sep 17 00:00:00 2001 From: meisnate12 <meisnate12@gmail.com> Date: Mon, 14 Mar 2022 15:41:07 -0400 Subject: [PATCH 21/32] [19] allow nested any and all plex_search for music --- VERSION | 2 +- modules/builder.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 98c90fdcc..2de0db69e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop18 +1.16.1-develop19 diff --git a/modules/builder.py b/modules/builder.py index 129563d09..eaf58f1cf 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -1622,7 +1622,7 @@ def build_url_arg(arg, mod=None, arg_s=None, mod_s=None): error = f"{self.Type} Error: {final_attr} {method} attribute only works for movie libraries" elif self.library.is_movie and final_attr in plex.show_only_searches: error = f"{self.Type} Error: {final_attr} {method} attribute only works for show libraries" - elif self.library.is_music and final_attr not in plex.music_searches: + elif self.library.is_music and final_attr not in plex.music_searches + ["all", "any"]: error = f"{self.Type} Error: {final_attr} {method} attribute does not work for music libraries" elif not self.library.is_music and final_attr in plex.music_searches: error = f"{self.Type} Error: {final_attr} {method} attribute only works for music libraries" From 7ba5d3b229a1e8ec396104f876d83a8a6f25b7c3 Mon Sep 17 00:00:00 2001 From: meisnate12 <meisnate12@gmail.com> Date: Mon, 14 Mar 2022 17:30:30 -0400 Subject: [PATCH 22/32] [20] fix keyError --- VERSION | 2 +- modules/cache.py | 2 ++ modules/plex.py | 1 + modules/sonarr.py | 2 +- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 2de0db69e..2f6d98f89 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop19 +1.16.1-develop20 diff --git a/modules/cache.py b/modules/cache.py index 82983b50b..d78b07d0b 100644 --- a/modules/cache.py +++ b/modules/cache.py @@ -493,6 +493,8 @@ def query_tmdb_show(self, tmdb_id, expiration): tmdb_dict["status"] = row["status"] if row["status"] else None tmdb_dict["type"] = row["type"] if row["type"] else None tmdb_dict["tvdb_id"] = row["tvdb_id"] if row["tvdb_id"] else None + tmdb_dict["countries"] = row["countries"] if row["countries"] else None + tmdb_dict["seasons"] = row["seasons"] if row["seasons"] else None datetime_object = datetime.strptime(row["expiration_date"], "%Y-%m-%d") time_between_insertion = datetime.now() - datetime_object expired = time_between_insertion.days > expiration diff --git a/modules/plex.py b/modules/plex.py index 8d10349e1..996e9865e 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -793,6 +793,7 @@ def get_collection_items(self, collection, smart_label_collection): def get_filter_items(self, uri_args): key = f"/library/sections/{self.Plex.key}/all{uri_args}" + logger.debug(key) return self.Plex._search(key, None, 0, plexapi.X_PLEX_CONTAINER_SIZE) def get_collection_name_and_items(self, collection, smart_label_collection): diff --git a/modules/sonarr.py b/modules/sonarr.py index e1bc3c17f..0195427ba 100644 --- a/modules/sonarr.py +++ b/modules/sonarr.py @@ -110,7 +110,7 @@ def mass_add(): invalid.extend(_i) except ArrException as e: logger.stacktrace() - raise Failed(f"Radarr Error: {e}") + raise Failed(f"Sonarr Error: {e}") for i, item in enumerate(tvdb_ids, 1): path = item[1] if isinstance(item, tuple) else None From 48ca18d4ceca2f7c4588699f68bbcc9a1ebb2afb Mon Sep 17 00:00:00 2001 From: meisnate12 <meisnate12@gmail.com> Date: Mon, 14 Mar 2022 20:11:16 -0400 Subject: [PATCH 23/32] [21] #739 add anidb as mass source --- VERSION | 2 +- docs/config/operations.md | 37 +++++++++--------- docs/metadata/dynamic.md | 4 ++ modules/anidb.py | 82 ++++++++++++++++++++++++++++++--------- modules/builder.py | 4 +- modules/config.py | 10 +++-- plex_meta_manager.py | 26 ++++++++++++- 7 files changed, 120 insertions(+), 45 deletions(-) diff --git a/VERSION b/VERSION index 2f6d98f89..73519de31 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop20 +1.16.1-develop21 diff --git a/docs/config/operations.md b/docs/config/operations.md index e2674e2cf..d845d5eb1 100644 --- a/docs/config/operations.md +++ b/docs/config/operations.md @@ -16,24 +16,25 @@ libraries: The available attributes for the operations attribute are as follows -| Attribute | Description | -|:--------------------------------------------------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `assets_for_all` | Search in assets for images for every item in your library.<br>**Values:** `true` or `false` | -| `delete_collections_with_less` | Deletes every collection with less than the given number of items.<br>**Values:** number greater then 0 | -| `delete_unmanaged_collections` | Deletes every unmanaged collection<br>**Values:** `true` or `false` | -| `mass_genre_update` | Updates every item's genres in the library to the chosen site's genres<br>**Values:** <table class="clearTable"><tr><td>`tmdb`</td><td>Use TMDb for Genres</td></tr><tr><td>`tvdb`</td><td>Use TVDb for Genres</td></tr><tr><td>`omdb`</td><td>Use IMDb through OMDb for Genres</td></tr></table> | -| `mass_content_rating_update` | Updates every item's content rating in the library to the chosen site's genres<br>**Values:** <table class="clearTable"><tr><td>`mdb`</td><td>Use MdbList for Content Ratings</td></tr><tr><td>`mdb_commonsense`</td><td>Use Commonsense Rating through MDbList for Content Ratings</td></tr><tr><td>`omdb`</td><td>Use IMDb through OMDb for Content Ratings</td></tr></table> | -| `mass_audience_rating_update`/<br>`mass_critic_rating_update` | Updates every item's audience/critic rating in the library to the chosen site's rating<br>**Values:** <table class="clearTable"><tr><td>`tmdb`</td><td>Use TMDb Rating</td></tr><tr><td>`omdb`</td><td>Use IMDbRating through OMDb</td></tr><tr><td>`mdb`</td><td>Use MdbList Score</td></tr><tr><td>`mdb_imdb`</td><td>Use IMDb Rating through MDbList</td></tr><tr><td>`mdb_metacritic`</td><td>Use Metacritic Rating through MDbList</td></tr><tr><td>`mdb_metacriticuser`</td><td>Use Metacritic User Rating through MDbList</td></tr><tr><td>`mdb_trakt`</td><td>Use Trakt Rating through MDbList</td></tr><tr><td>`mdb_tomatoes`</td><td>Use Rotten Tomatoes Rating through MDbList</td></tr><tr><td>`mdb_tomatoesaudience`</td><td>Use Rotten Tomatoes Audience Rating through MDbList</td></tr><tr><td>`mdb_tmdb`</td><td>Use TMDb Rating through MDbList</td></tr><tr><td>`mdb_letterboxd`</td><td>Use Letterboxd Rating through MDbList</td></tr></table> | -| `mass_trakt_rating_update` | Updates every movie/show's user rating in the library to match your custom rating on Trakt if there is one<br>**Values:** `true` or `false` | -| `mass_collection_mode` | Updates every Collection in your library to the specified Collection Mode<br>**Values:** `default`: Library default<br>`hide`: Hide Collection<br>`hide_items`: Hide Items in this Collection<br>`show_items`: Show this Collection and its Items<table class="clearTable"><tr><td>`default`</td><td>Library default</td></tr><tr><td>`hide`</td><td>Hide Collection</td></tr><tr><td>`hide_items`</td><td>Hide Items in this Collection</td></tr><tr><td>`show_items`</td><td>Show this Collection and its Items</td></tr></table> | -| `update_blank_track_titles ` | Search though every track in a music library and replace any blank track titles with the tracks sort title<br>**Values:** `true` or `false` | -| `split_duplicates` | Splits all duplicate movies/shows found in this library<br>**Values:** `true` or `false` | -| `radarr_add_all` | Adds every item in the library to Radarr. The existing paths in plex will be used as the root folder of each item, if the paths in Plex are not the same as your Radarr paths you can use the `plex_path` and `radarr_path` [Radarr](radarr) details to convert the paths.<br>**Values:** `true` or `false` | -| `radarr_remove_by_tag` | Removes every item from Radarr with the Tags given<br>**Values:** List or comma separated string of tags | -| `sonarr_add_all` | Adds every item in the library to Sonarr. The existing paths in plex will be used as the root folder of each item, if the paths in Plex are not the same as your Sonarr paths you can use the `plex_path` and `sonarr_path` [Sonarr](sonarr) details to convert the paths.<br>**Values:** `true` or `false` | -| `sonarr_remove_by_tag` | Removes every item from Sonarr with the Tags given<br>**Values:** List or comma separated string of tags | -| `genre_mapper` | Allows genres to be changed to other genres or be removed from every item in your library.<br>**Values:** [see below for usage](#genre-mapper) | -| `metadata_backup` | Creates/Maintains a PMM [Metadata File](../metadata/metadata) with a full `metadata` mapping based on the library's items locked attributes.<br>**Values:** [see below for usage](#metadata-backup) | +| Attribute | Description | +|:--------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `assets_for_all` | Search in assets for images for every item in your library.<br>**Values:** `true` or `false` | +| `delete_collections_with_less` | Deletes every collection with less than the given number of items.<br>**Values:** number greater then 0 | +| `delete_unmanaged_collections` | Deletes every unmanaged collection<br>**Values:** `true` or `false` | +| `mass_genre_update` | Updates every item's genres in the library to the chosen site's genres<br>**Values:** <table class="clearTable"><tr><td>`tmdb`</td><td>Use TMDb for Genres</td></tr><tr><td>`tvdb`</td><td>Use TVDb for Genres</td></tr><tr><td>`omdb`</td><td>Use IMDb through OMDb for Genres</td></tr><tr><td>`anidb`</td><td>Use AniDB Tags for Genres</td></tr></table> | +| `mass_content_rating_update` | Updates every item's content rating in the library to the chosen site's genres<br>**Values:** <table class="clearTable"><tr><td>`mdb`</td><td>Use MdbList for Content Ratings</td></tr><tr><td>`mdb_commonsense`</td><td>Use Commonsense Rating through MDbList for Content Ratings</td></tr><tr><td>`omdb`</td><td>Use IMDb through OMDb for Content Ratings</td></tr></table> | +| `mass_originally_available_update` | Updates every item's originally available date in the library to the chosen site's date<br>**Values:** <table class="clearTable"><tr><td>`tmdb`</td><td>Use TMDb Release Date</td></tr><tr><td>`tvdb`</td><td>Use TVDb Release Date</td></tr><tr><td>`omdb`</td><td>Use IMDb Release Date through OMDb</td></tr><tr><td>`mdb`</td><td>Use MdbList Release Date</td></tr><tr><td>`anidb`</td><td>Use AniDB Release Date</td></tr></table> | +| `mass_audience_rating_update`/<br>`mass_critic_rating_update` | Updates every item's audience/critic rating in the library to the chosen site's rating<br>**Values:** <table class="clearTable"><tr><td>`tmdb`</td><td>Use TMDb Rating</td></tr><tr><td>`omdb`</td><td>Use IMDbRating through OMDb</td></tr><tr><td>`mdb`</td><td>Use MdbList Score</td></tr><tr><td>`mdb_imdb`</td><td>Use IMDb Rating through MDbList</td></tr><tr><td>`mdb_metacritic`</td><td>Use Metacritic Rating through MDbList</td></tr><tr><td>`mdb_metacriticuser`</td><td>Use Metacritic User Rating through MDbList</td></tr><tr><td>`mdb_trakt`</td><td>Use Trakt Rating through MDbList</td></tr><tr><td>`mdb_tomatoes`</td><td>Use Rotten Tomatoes Rating through MDbList</td></tr><tr><td>`mdb_tomatoesaudience`</td><td>Use Rotten Tomatoes Audience Rating through MDbList</td></tr><tr><td>`mdb_tmdb`</td><td>Use TMDb Rating through MDbList</td></tr><tr><td>`mdb_letterboxd`</td><td>Use Letterboxd Rating through MDbList</td></tr><tr><td>`anidb_rating`</td><td>Use AniDB Rating</td></tr><tr><td>`anidb_average`</td><td>Use AniDB Average</td></tr></table> | +| `mass_trakt_rating_update` | Updates every movie/show's user rating in the library to match your custom rating on Trakt if there is one<br>**Values:** `true` or `false` | +| `mass_collection_mode` | Updates every Collection in your library to the specified Collection Mode<br>**Values:** `default`: Library default<br>`hide`: Hide Collection<br>`hide_items`: Hide Items in this Collection<br>`show_items`: Show this Collection and its Items<table class="clearTable"><tr><td>`default`</td><td>Library default</td></tr><tr><td>`hide`</td><td>Hide Collection</td></tr><tr><td>`hide_items`</td><td>Hide Items in this Collection</td></tr><tr><td>`show_items`</td><td>Show this Collection and its Items</td></tr></table> | +| `update_blank_track_titles ` | Search though every track in a music library and replace any blank track titles with the tracks sort title<br>**Values:** `true` or `false` | +| `split_duplicates` | Splits all duplicate movies/shows found in this library<br>**Values:** `true` or `false` | +| `radarr_add_all` | Adds every item in the library to Radarr. The existing paths in plex will be used as the root folder of each item, if the paths in Plex are not the same as your Radarr paths you can use the `plex_path` and `radarr_path` [Radarr](radarr) details to convert the paths.<br>**Values:** `true` or `false` | +| `radarr_remove_by_tag` | Removes every item from Radarr with the Tags given<br>**Values:** List or comma separated string of tags | +| `sonarr_add_all` | Adds every item in the library to Sonarr. The existing paths in plex will be used as the root folder of each item, if the paths in Plex are not the same as your Sonarr paths you can use the `plex_path` and `sonarr_path` [Sonarr](sonarr) details to convert the paths.<br>**Values:** `true` or `false` | +| `sonarr_remove_by_tag` | Removes every item from Sonarr with the Tags given<br>**Values:** List or comma separated string of tags | +| `genre_mapper` | Allows genres to be changed to other genres or be removed from every item in your library.<br>**Values:** [see below for usage](#genre-mapper) | +| `metadata_backup` | Creates/Maintains a PMM [Metadata File](../metadata/metadata) with a full `metadata` mapping based on the library's items locked attributes.<br>**Values:** [see below for usage](#metadata-backup) | ## Genre Mapper diff --git a/docs/metadata/dynamic.md b/docs/metadata/dynamic.md index 23118240c..a90396936 100644 --- a/docs/metadata/dynamic.md +++ b/docs/metadata/dynamic.md @@ -1538,6 +1538,10 @@ dynamic_collections: Name of the template to use for these dynamic collections. Each `type` has its own default template, but if you want to define and use your own template you can. +Each template is passed a template variable whose name matches the dynamic collection `type`. i.e. in the example below `<<network>>` is the template variable. + +`key` and `key_name` are both passed along and can be used as template variables. + For example, the template below removes the limit on the `smart_filter` so it shows all items in each network ```yaml diff --git a/modules/anidb.py b/modules/anidb.py index 0c0a45399..7b76ffaab 100644 --- a/modules/anidb.py +++ b/modules/anidb.py @@ -1,4 +1,5 @@ import time +from datetime import datetime from modules import util from modules.util import Failed @@ -14,9 +15,51 @@ "login": f"{base_url}/perl-bin/animedb.pl" } +class AniDBObj: + def __init__(self, anidb, anidb_id, language): + self.anidb = anidb + self.anidb_id = anidb_id + self.language = language + response = self.anidb._request(f"{urls['anime']}/{anidb_id}", language=self.language) + + def parse_page(xpath, is_list=False, is_float=False, is_date=False, fail=False): + parse_results = response.xpath(xpath) + try: + if len(parse_results) > 0: + parse_results = [r.strip() for r in parse_results if len(r) > 0] + if parse_results: + if is_list: + return parse_results + elif is_float: + return float(parse_results[0]) + elif is_date: + return datetime.strptime(parse_results[0], "%d.%m.%Y") + else: + return parse_results[0] + except (ValueError, TypeError): + pass + if fail: + raise Failed(f"AniDB Error: No Anime Found for AniDB ID: {self.anidb_id}") + elif is_list: + return [] + elif is_float: + return 0 + else: + return None + + self.official_title = parse_page(f"//th[text()='Main Title']/parent::tr/td/span/text()", fail=True) + self.title = parse_page(f"//th[text()='Official Title']/parent::tr/td/span/span/span[text()='{self.language}']/parent::span/parent::span/parent::td/label/text()") + self.rating = parse_page(f"//th[text()='Rating']/parent::tr/td/span/a/span/text()", is_float=True) + self.average = parse_page(f"//th[text()='Average']/parent::tr/td/span/a/span/text()", is_float=True) + self.released = parse_page(f"//th[text()='Year']/parent::tr/td/span/text()", is_date=True) + self.tags = [g.capitalize() for g in parse_page("//th/a[text()='Tags']/parent::th/parent::tr/td/span/a/span/text()", is_list=True)] + self.description = response.xpath(f"string(//div[@itemprop='description'])") + + class AniDB: - def __init__(self, config): + def __init__(self, config, language): self.config = config + self.language = language self.username = None self.password = None @@ -29,46 +72,46 @@ def login(self, username, password): if not self._request(urls["login"], data=data).xpath("//li[@class='sub-menu my']/@title"): raise Failed("AniDB Error: Login failed") - def _request(self, url, language=None, data=None): + def _request(self, url, data=None): if self.config.trace_mode: logger.debug(f"URL: {url}") if data: - return self.config.post_html(url, data=data, headers=util.header(language)) + return self.config.post_html(url, data=data, headers=util.header(self.language)) else: - return self.config.get_html(url, headers=util.header(language)) + return self.config.get_html(url, headers=util.header(self.language)) - def _popular(self, language): - response = self._request(urls["popular"], language=language) + def _popular(self): + response = self._request(urls["popular"]) return util.get_int_list(response.xpath("//td[@class='name anime']/a/@href"), "AniDB ID") - def _relations(self, anidb_id, language): - response = self._request(f"{urls['anime']}/{anidb_id}{urls['relation']}", language=language) + def _relations(self, anidb_id): + response = self._request(f"{urls['anime']}/{anidb_id}{urls['relation']}") return util.get_int_list(response.xpath("//area/@href"), "AniDB ID") - def _validate(self, anidb_id, language): - response = self._request(f"{urls['anime']}/{anidb_id}", language=language) + def _validate(self, anidb_id): + response = self._request(f"{urls['anime']}/{anidb_id}") ids = response.xpath(f"//*[text()='a{anidb_id}']/text()") if len(ids) > 0: return util.regex_first_int(ids[0], "AniDB ID") raise Failed(f"AniDB Error: AniDB ID: {anidb_id} not found") - def validate_anidb_ids(self, anidb_ids, language): + def validate_anidb_ids(self, anidb_ids): anidb_list = util.get_int_list(anidb_ids, "AniDB ID") anidb_values = [] for anidb_id in anidb_list: try: - anidb_values.append(self._validate(anidb_id, language)) + anidb_values.append(self._validate(anidb_id)) except Failed as e: logger.error(e) if len(anidb_values) > 0: return anidb_values raise Failed(f"AniDB Error: No valid AniDB IDs in {anidb_list}") - def _tag(self, tag, limit, language): + def _tag(self, tag, limit): anidb_ids = [] current_url = f"{urls['tag']}/{tag}" while True: - response = self._request(current_url, language=language) + response = self._request(current_url) anidb_ids.extend(util.get_int_list(response.xpath("//td[@class='name main anime']/a/@href"), "AniDB ID")) next_page_list = response.xpath("//li[@class='next']/a/@href") if len(anidb_ids) >= limit or len(next_page_list) == 0: @@ -77,20 +120,23 @@ def _tag(self, tag, limit, language): current_url = f"{base_url}{next_page_list[0]}" return anidb_ids[:limit] - def get_anidb_ids(self, method, data, language): + def get_anime(self, anidb_id): + return AniDBObj(self, anidb_id, self.language) + + def get_anidb_ids(self, method, data): anidb_ids = [] if method == "anidb_popular": logger.info(f"Processing AniDB Popular: {data} Anime") - anidb_ids.extend(self._popular(language)[:data]) + anidb_ids.extend(self._popular()[:data]) elif method == "anidb_tag": logger.info(f"Processing AniDB Tag: {data['limit'] if data['limit'] > 0 else 'All'} Anime from the Tag ID: {data['tag']}") - anidb_ids = self._tag(data["tag"], data["limit"], language) + anidb_ids = self._tag(data["tag"], data["limit"]) elif method == "anidb_id": logger.info(f"Processing AniDB ID: {data}") anidb_ids.append(data) elif method == "anidb_relation": logger.info(f"Processing AniDB Relation: {data}") - anidb_ids.extend(self._relations(data, language)) + anidb_ids.extend(self._relations(data)) else: raise Failed(f"AniDB Error: Method {method} not supported") logger.debug("") diff --git a/modules/builder.py b/modules/builder.py index eaf58f1cf..d8fe56f02 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -934,7 +934,7 @@ def _anidb(self, method_name, method_data): if method_name == "anidb_popular": self.builders.append((method_name, util.parse(self.Type, method_name, method_data, datatype="int", default=30, maximum=30))) elif method_name in ["anidb_id", "anidb_relation"]: - for anidb_id in self.config.AniDB.validate_anidb_ids(method_data, self.language): + for anidb_id in self.config.AniDB.validate_anidb_ids(method_data): self.builders.append((method_name, anidb_id)) elif method_name == "anidb_tag": for dict_data in util.parse(self.Type, method_name, method_data, datatype="listdict"): @@ -1326,7 +1326,7 @@ def gather_ids(self, method, value): elif "tautulli" in method: ids = self.library.Tautulli.get_rating_keys(self.library, value, self.playlist) elif "anidb" in method: - anidb_ids = self.config.AniDB.get_anidb_ids(method, value, self.language) + anidb_ids = self.config.AniDB.get_anidb_ids(method, value) ids = self.config.Convert.anidb_to_ids(anidb_ids, self.library) elif "anilist" in method: anilist_ids = self.config.AniList.get_anilist_ids(method, value) diff --git a/modules/config.py b/modules/config.py index 4289b1486..5afb4db61 100644 --- a/modules/config.py +++ b/modules/config.py @@ -31,9 +31,9 @@ logger = util.logger sync_modes = {"append": "Only Add Items to the Collection or Playlist", "sync": "Add & Remove Items from the Collection or Playlist"} -mass_genre_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb", "tvdb": "Use TVDb Metadata"} +mass_genre_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb", "tvdb": "Use TVDb Metadata", "anidb": "Use AniDB Tag Metadata"} mass_content_options = {"omdb": "Use IMDb Metadata through OMDb", "mdb": "Use MdbList Metadata", "mdb_commonsense": "Use Commonsense Rating through MDbList"} -mass_available_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb", "mdb": "Use MdbList Metadata", "tvdb": "Use TVDb Metadata"} +mass_available_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb", "mdb": "Use MdbList Metadata", "tvdb": "Use TVDb Metadata", "anidb": "Use AniDB Metadata"} mass_rating_options = { "tmdb": "Use TMDb Rating", "omdb": "Use IMDb Rating through OMDb", @@ -45,7 +45,9 @@ "mdb_tomatoes": "Use Rotten Tomatoes Rating through MDbList", "mdb_tomatoesaudience": "Use Rotten Tomatoes Audience Rating through MDbList", "mdb_tmdb": "Use TMDb Rating through MDbList", - "mdb_letterboxd": "Use Letterboxd Rating through MDbList" + "mdb_letterboxd": "Use Letterboxd Rating through MDbList", + "anidb_rating": "Use AniDB Rating", + "anidb_average": "Use AniDB Average" } class ConfigFile: @@ -447,7 +449,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No else: logger.warning("mal attribute not found") - self.AniDB = AniDB(self) + self.AniDB = AniDB(self, check_for_attribute(self.data, "language", parent="anidb", default="en")) if "anidb" in self.data: logger.separator() logger.info("Connecting to AniDB...") diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 7cdfca876..73879294b 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -448,6 +448,10 @@ def library_operations(config, library): sonarr_adds = [] trakt_ratings = config.Trakt.user_ratings(library.is_movie) if library.mass_trakt_rating_update else [] + reverse_anidb = {} + for k, v in library.anidb_map.values(): + reverse_anidb[v] = k + for i, item in enumerate(items, 1): try: library.reload(item) @@ -518,6 +522,16 @@ def library_operations(config, library): else: logger.info(f"{item.title[:25]:<25} | No TVDb ID for Guid: {item.guid}") + anidb_item = None + if library.mass_genre_update == "anidb": + if item.ratingKey in reverse_anidb: + try: + anidb_item = config.AniDB.get_anime(reverse_anidb[item.ratingKey]) + except Failed as e: + logger.error(str(e)) + else: + logger.info(f"{item.title[:25]:<25} | No AniDB ID for Guid: {item.guid}") + mdb_item = None if library.mass_audience_rating_update in util.mdb_types or library.mass_critic_rating_update in util.mdb_types \ or library.mass_content_rating_update in ["mdb", "mdb_commonsense"] or library.mass_originally_available_update == "mdb": @@ -563,6 +577,10 @@ def get_rating(attribute): return mdb_item.tmdb_rating / 10 if mdb_item.tmdb_rating else None elif mdb_item and attribute == "mdb_letterboxd": return mdb_item.letterboxd_rating * 2 if mdb_item.letterboxd_rating else None + elif anidb_item and attribute == "anidb_rating": + return anidb_item.rating + elif anidb_item and attribute == "anidb_average": + return anidb_item.average else: raise Failed @@ -574,6 +592,8 @@ def get_rating(attribute): new_genres = omdb_item.genres elif tvdb_item and library.mass_genre_update == "tvdb": new_genres = tvdb_item.genres + elif anidb_item and library.mass_genre_update == "anidb": + new_genres = anidb_item.genres else: raise Failed library.edit_tags("genre", item, sync_tags=new_genres) @@ -622,10 +642,12 @@ def get_rating(attribute): new_date = omdb_item.released elif mdb_item and library.mass_originally_available_update == "mdb": new_date = mdb_item.released - elif tvdb_item and library.mass_content_rating_update == "tvdb": + elif tvdb_item and library.mass_originally_available_update == "tvdb": new_date = tvdb_item.released - elif tmdb_item and library.mass_content_rating_update == "tvdb": + elif tmdb_item and library.mass_originally_available_update == "tmdb": new_date = tmdb_item.release_date if library.is_movie else tmdb_item.first_air_date + elif anidb_item and library.mass_originally_available_update == "anidb": + new_date = anidb_item.released else: raise Failed if new_date is None: From edb752a40368463c0c7413911b72026d4bf61151 Mon Sep 17 00:00:00 2001 From: meisnate12 <meisnate12@gmail.com> Date: Mon, 14 Mar 2022 22:40:09 -0400 Subject: [PATCH 24/32] [22] fix genre_mapper and content_rating_mapper --- VERSION | 2 +- plex_meta_manager.py | 87 +++++++++++++++++++++++--------------------- 2 files changed, 46 insertions(+), 43 deletions(-) diff --git a/VERSION b/VERSION index 73519de31..6f4702bf4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop21 +1.16.1-develop22 diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 73879294b..c3f2f3ae5 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -584,18 +584,33 @@ def get_rating(attribute): else: raise Failed - if library.mass_genre_update: + if library.mass_genre_update or library.genre_mapper: try: - if tmdb_item and library.mass_genre_update == "tmdb": - new_genres = tmdb_item.genres - elif omdb_item and library.mass_genre_update == "omdb": - new_genres = omdb_item.genres - elif tvdb_item and library.mass_genre_update == "tvdb": - new_genres = tvdb_item.genres - elif anidb_item and library.mass_genre_update == "anidb": - new_genres = anidb_item.genres - else: - raise Failed + new_genres = [] + if library.mass_genre_update: + if tmdb_item and library.mass_genre_update == "tmdb": + new_genres = tmdb_item.genres + elif omdb_item and library.mass_genre_update == "omdb": + new_genres = omdb_item.genres + elif tvdb_item and library.mass_genre_update == "tvdb": + new_genres = tvdb_item.genres + elif anidb_item and library.mass_genre_update == "anidb": + new_genres = anidb_item.genres + else: + raise Failed + if not new_genres: + logger.info(f"{item.title[:25]:<25} | No Genres Found") + if library.genre_mapper: + if not new_genres: + new_genres = [g.tag for g in item.genres] + mapped_genres = [] + for genre in new_genres: + if genre in library.genre_mapper: + if library.genre_mapper[genre]: + mapped_genres.append(library.genre_mapper[genre]) + else: + mapped_genres.append(genre) + new_genres = mapped_genres library.edit_tags("genre", item, sync_tags=new_genres) except Failed: pass @@ -619,19 +634,26 @@ def get_rating(attribute): logger.info(f"{item.title[:25]:<25} | Critic Rating | {new_rating}") except Failed: pass - if library.mass_content_rating_update: + if library.mass_content_rating_update or library.content_rating_mapper: try: - if omdb_item and library.mass_content_rating_update == "omdb": - new_rating = omdb_item.content_rating - elif mdb_item and library.mass_content_rating_update == "mdb": - new_rating = mdb_item.certification if mdb_item.certification else None - elif mdb_item and library.mass_content_rating_update == "mdb_commonsense": - new_rating = mdb_item.commonsense if mdb_item.commonsense else None - else: - raise Failed - if new_rating is None: - logger.info(f"{item.title[:25]:<25} | No Content Rating Found") - elif str(item.rating) != str(new_rating): + new_rating = None + if library.mass_content_rating_update: + if omdb_item and library.mass_content_rating_update == "omdb": + new_rating = omdb_item.content_rating + elif mdb_item and library.mass_content_rating_update == "mdb": + new_rating = mdb_item.certification if mdb_item.certification else None + elif mdb_item and library.mass_content_rating_update == "mdb_commonsense": + new_rating = mdb_item.commonsense if mdb_item.commonsense else None + else: + raise Failed + if new_rating is None: + logger.info(f"{item.title[:25]:<25} | No Content Rating Found") + if library.content_rating_mapper: + if new_rating is None: + new_rating = item.contentRating + if new_rating in library.content_rating_mapper: + new_rating = library.content_rating_mapper[new_rating] + if str(item.contentRating) != str(new_rating): library.edit_query(item, {"contentRating.value": new_rating, "contentRating.locked": 1}) logger.info(f"{item.title[:25]:<25} | Content Rating | {new_rating}") except Failed: @@ -658,25 +680,6 @@ def get_rating(attribute): except Failed: pass - if library.genre_mapper or library.content_rating_mapper: - try: - library.reload(item) - if library.genre_mapper: - adds = [] - deletes = [] - for genre in item.genres: - if genre.tag in library.genre_mapper: - deletes.append(genre.tag) - if library.genre_mapper[genre.tag]: - adds.append(library.genre_mapper[genre.tag]) - library.edit_tags("genre", item, add_tags=adds, remove_tags=deletes) - if library.content_rating_mapper: - if item.contentRating in library.content_rating_mapper: - library.edit_query(item, {"contentRating.value": library.content_rating_mapper[item.contentRating], "contentRating.locked": 1}) - logger.info(f"{item.title[:25]:<25} | Content Rating | {library.content_rating_mapper[item.contentRating]}") - except Failed: - pass - if library.Radarr and library.radarr_add_all_existing: try: library.Radarr.add_tmdb(radarr_adds) From 29ab86267b7694b617f178c34b433ef7375ec3d8 Mon Sep 17 00:00:00 2001 From: meisnate12 <meisnate12@gmail.com> Date: Tue, 15 Mar 2022 02:15:41 -0400 Subject: [PATCH 25/32] [23] fix mappers --- VERSION | 2 +- docs/config/operations.md | 51 ++++++++++++++++++++++++++++----------- modules/config.py | 28 ++++++++------------- 3 files changed, 48 insertions(+), 33 deletions(-) diff --git a/VERSION b/VERSION index 6f4702bf4..d7f503257 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop22 +1.16.1-develop23 diff --git a/docs/config/operations.md b/docs/config/operations.md index d845d5eb1..5b663c5c0 100644 --- a/docs/config/operations.md +++ b/docs/config/operations.md @@ -34,6 +34,7 @@ The available attributes for the operations attribute are as follows | `sonarr_add_all` | Adds every item in the library to Sonarr. The existing paths in plex will be used as the root folder of each item, if the paths in Plex are not the same as your Sonarr paths you can use the `plex_path` and `sonarr_path` [Sonarr](sonarr) details to convert the paths.<br>**Values:** `true` or `false` | | `sonarr_remove_by_tag` | Removes every item from Sonarr with the Tags given<br>**Values:** List or comma separated string of tags | | `genre_mapper` | Allows genres to be changed to other genres or be removed from every item in your library.<br>**Values:** [see below for usage](#genre-mapper) | +| `content_rating_mapper` | Allows content ratings to be changed to other content ratings or be removed from every item in your library.<br>**Values:** [see below for usage](#content-rating-mapper) | | `metadata_backup` | Creates/Maintains a PMM [Metadata File](../metadata/metadata) with a full `metadata` mapping based on the library's items locked attributes.<br>**Values:** [see below for usage](#metadata-backup) | ## Genre Mapper @@ -41,8 +42,8 @@ The available attributes for the operations attribute are as follows You can use the `genre_mapper` operation to map genres in your library. Each attribute under `genre_mapper` is a separate mapping and has two parts. -* The key (`Action` in the example below) is what the genres will end up as. -* The value(`Action/Adventure, Action & Adventure` in the example below) is what genres you want mapped to the key. +* The key (`Action/Adventure, Action & Adventure` in the example below) is what genres you want mapped to the value. +* The value (`Action` in the example below) is what the genres will end up as. So this example will change go through every item in your library and change the genre `Action/Adventure` or `Action & Adventure` to `Action` and `Romantic Comedy` to `Comedy`. @@ -51,36 +52,58 @@ library: Movies: operations: genre_mapper: - Action: Action/Adventure, Action & Adventure - Comedy: Romantic Comedy + "Action/Adventure": Action + "Action & Adventure": Action + Romantic Comedy: Comedy ``` -you can also use a list: +To just Remove a Genre without replacing it just set the Genre to nothing like this. ```yaml library: Movies: operations: genre_mapper: - Action: - - Action/Adventure - - Action & Adventure - Comedy: Romantic Comedy + "Action/Adventure": Action + "Action & Adventure": Action + Romantic Comedy: ``` -To just Remove a Genre without replacing it just set the Genre to nothing like this. +This example will change go through every item in your library and change the genre `Action/Adventure` or `Action & Adventure` to `Action` and remove every instance of the Genre `Romantic Comedy`. + +## Content Rating Mapper + +You can use the `content_rating_mapper` operation to map content ratings in your library. + +Each attribute under `content_rating_mapper` is a separate mapping and has two parts. +* The key (`PG`, `PG-13` in the example below) is what content ratings you want mapped to the value. +* The value (`Y-10` in the example below) is what the content ratings will end up as. + +So this example will change go through every item in your library and change the content rating `PG` or `PG-13` to `Y-10` and `R` to `Y-17`. ```yaml library: Movies: operations: - genre_mapper: - Action: Action/Adventure, Action & Adventure - Romantic Comedy: + content_rating_mapper: + PG: Y-10 + "PG-13": Y-10 + R: Y-17 ``` -This example will change go through every item in your library and change the genre `Action/Adventure` or `Action & Adventure` to `Action` and remove every instance of the Genre `Romantic Comedy`. +To just Remove a content rating without replacing it just set the content rating to nothing like this. + +```yaml +library: + Movies: + operations: + content_rating_mapper: + PG: Y-10 + "PG-13": Y-10 + R: +``` +This example will change go through every item in your library and change the content rating `PG` or `PG-13` to `Y-10` and remove every instance of the content rating `R`. ## Metadata Backup diff --git a/modules/config.py b/modules/config.py index 5afb4db61..a19e1366e 100644 --- a/modules/config.py +++ b/modules/config.py @@ -730,30 +730,22 @@ def check_dict(attr): logger.error("Config Error: tmdb_collections blank using default settings") if "genre_mapper" in lib["operations"]: if lib["operations"]["genre_mapper"] and isinstance(lib["operations"]["genre_mapper"], dict): - params["genre_mapper"] = {} - for new_genre, old_genres in lib["operations"]["genre_mapper"].items(): - if old_genres is None: - params["genre_mapper"][new_genre] = old_genres + params["genre_mapper"] = lib["operations"]["genre_mapper"] + for old_genre, new_genre in lib["operations"]["genre_mapper"].items(): + if old_genre == new_genre: + logger.error("Config Error: genres cannot be mapped to themselves") else: - for old_genre in util.get_list(old_genres): - if old_genre == new_genre: - logger.error("Config Error: genres cannot be mapped to themselves") - else: - params["genre_mapper"][old_genre] = new_genre + params["genre_mapper"][old_genre] = new_genre if new_genre else None else: logger.error("Config Error: genre_mapper is blank") if "content_rating_mapper" in lib["operations"]: if lib["operations"]["content_rating_mapper"] and isinstance(lib["operations"]["content_rating_mapper"], dict): - params["content_rating_mapper"] = {} - for new_rating, old_ratings in lib["operations"]["content_rating_mapper"].items(): - if old_ratings is None: - params["content_rating_mapper"][new_rating] = old_ratings + params["content_rating_mapper"] = lib["operations"]["content_rating_mapper"] + for old_content, new_content in lib["operations"]["content_rating_mapper"].items(): + if old_content == new_content: + logger.error("Config Error: content rating cannot be mapped to themselves") else: - for old_rating in util.get_list(old_ratings): - if old_rating == new_rating: - logger.error("Config Error: Content Ratings cannot be mapped to themselves") - else: - params["content_rating_mapper"][old_rating] = new_rating + params["content_rating_mapper"][old_content] = new_content if new_content else None else: logger.error("Config Error: content_rating_mapper is blank") if "genre_collections" in lib["operations"]: From 99c4d46f8b86edd6e7872953656c2fff57be2fcf Mon Sep 17 00:00:00 2001 From: Frazzer951 <luke343279@gmail.com> Date: Mon, 14 Mar 2022 17:36:09 -0700 Subject: [PATCH 26/32] Fix variable type for Sort in AniList Userlist --- modules/anilist.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/anilist.py b/modules/anilist.py index 68f85f16d..757f24756 100644 --- a/modules/anilist.py +++ b/modules/anilist.py @@ -221,7 +221,7 @@ def _relations(self, anilist_id, ignore_ids=None): def _userlist(self, username, list_name, sort_by): query = """ - query ($user: String, $sort: MediaListStatus) { + query ($user: String, $sort: [MediaListSort]) { MediaListCollection (userName: $user, sort: $sort, type: ANIME) { lists { name From 86f0dfaaedd39200a5c4c40c12e9be7e17001ee7 Mon Sep 17 00:00:00 2001 From: meisnate12 <meisnate12@gmail.com> Date: Tue, 15 Mar 2022 02:47:50 -0400 Subject: [PATCH 27/32] [24] Version Warning --- VERSION | 2 +- plex_meta_manager.py | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index d7f503257..1741532c2 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop23 +1.16.1-develop24 diff --git a/plex_meta_manager.py b/plex_meta_manager.py index c3f2f3ae5..491e0d2bd 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -2,7 +2,7 @@ from datetime import datetime try: - import plexapi, schedule + import plexapi, requests, schedule from modules.logs import MyLogger from plexapi.exceptions import NotFound from plexapi.video import Show, Season @@ -116,6 +116,10 @@ def my_except_hook(exctype, value, tb): version = line break +is_develop = "develop" in version +version_url = f"https://raw.githubusercontent.com/meisnate12/Plex-Meta-Manager/{'develop' if is_develop else 'master'}/VERSION" +newest_version = requests.get(version_url).content.decode().strip() + plexapi.BASE_HEADERS['X-Plex-Client-Identifier'] = "Plex-Meta-Manager" def start(attrs): @@ -129,6 +133,8 @@ def start(attrs): logger.info_center("|_| |_|\\___/_/\\_\\ |_| |_|\\___|\\__\\__,_| |_| |_|\\__,_|_| |_|\\__,_|\\__, |\\___|_| ") logger.info_center(" |___/ ") logger.info(f" Version: {version}") + if version != newest_version and ((is_develop and int(version[version.index("develop") + 7:]) < int(newest_version[newest_version.index("develop") + 7:])) or not is_develop): + logger.info(f" Newest Version: {newest_version}") if "time" in attrs and attrs["time"]: start_type = f"{attrs['time']} " elif "test" in attrs and attrs["test"]: start_type = "Test " elif "collections" in attrs and attrs["collections"]: start_type = "Collections " @@ -185,7 +191,10 @@ def start(attrs): except Failed as e: logger.stacktrace() logger.error(f"Webhooks Error: {e}") - logger.separator(f"Finished {start_type}Run\nFinished: {end_time.strftime('%H:%M:%S %Y-%m-%d')} Run Time: {run_time}") + version_line = f"Version: {version}" + if version != newest_version and ((is_develop and int(version[version.index("develop") + 7:]) < int(newest_version[newest_version.index("develop") + 7:])) or not is_develop): + version_line = f"{version_line} Newest Version: {newest_version}" + logger.separator(f"Finished {start_type}Run\n{version_line}\nFinished: {end_time.strftime('%H:%M:%S %Y-%m-%d')} Run Time: {run_time}") logger.remove_main_handler() def update_libraries(config): From 463e21082ed4408eea32a6175312d33723ba5547 Mon Sep 17 00:00:00 2001 From: meisnate12 <meisnate12@gmail.com> Date: Tue, 15 Mar 2022 11:52:55 -0400 Subject: [PATCH 28/32] [25] #756 added mass_imdb_parental_labels operation --- VERSION | 2 +- docs/config/operations.md | 1 + modules/cache.py | 41 +++++++++++++++++++++++++++++++++++++++ modules/config.py | 6 +++++- modules/imdb.py | 18 +++++++++++++++++ modules/library.py | 3 ++- plex_meta_manager.py | 8 ++++++++ 7 files changed, 76 insertions(+), 3 deletions(-) diff --git a/VERSION b/VERSION index 1741532c2..10599f2a4 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop24 +1.16.1-develop25 diff --git a/docs/config/operations.md b/docs/config/operations.md index 5b663c5c0..86f58f7af 100644 --- a/docs/config/operations.md +++ b/docs/config/operations.md @@ -25,6 +25,7 @@ The available attributes for the operations attribute are as follows | `mass_content_rating_update` | Updates every item's content rating in the library to the chosen site's genres<br>**Values:** <table class="clearTable"><tr><td>`mdb`</td><td>Use MdbList for Content Ratings</td></tr><tr><td>`mdb_commonsense`</td><td>Use Commonsense Rating through MDbList for Content Ratings</td></tr><tr><td>`omdb`</td><td>Use IMDb through OMDb for Content Ratings</td></tr></table> | | `mass_originally_available_update` | Updates every item's originally available date in the library to the chosen site's date<br>**Values:** <table class="clearTable"><tr><td>`tmdb`</td><td>Use TMDb Release Date</td></tr><tr><td>`tvdb`</td><td>Use TVDb Release Date</td></tr><tr><td>`omdb`</td><td>Use IMDb Release Date through OMDb</td></tr><tr><td>`mdb`</td><td>Use MdbList Release Date</td></tr><tr><td>`anidb`</td><td>Use AniDB Release Date</td></tr></table> | | `mass_audience_rating_update`/<br>`mass_critic_rating_update` | Updates every item's audience/critic rating in the library to the chosen site's rating<br>**Values:** <table class="clearTable"><tr><td>`tmdb`</td><td>Use TMDb Rating</td></tr><tr><td>`omdb`</td><td>Use IMDbRating through OMDb</td></tr><tr><td>`mdb`</td><td>Use MdbList Score</td></tr><tr><td>`mdb_imdb`</td><td>Use IMDb Rating through MDbList</td></tr><tr><td>`mdb_metacritic`</td><td>Use Metacritic Rating through MDbList</td></tr><tr><td>`mdb_metacriticuser`</td><td>Use Metacritic User Rating through MDbList</td></tr><tr><td>`mdb_trakt`</td><td>Use Trakt Rating through MDbList</td></tr><tr><td>`mdb_tomatoes`</td><td>Use Rotten Tomatoes Rating through MDbList</td></tr><tr><td>`mdb_tomatoesaudience`</td><td>Use Rotten Tomatoes Audience Rating through MDbList</td></tr><tr><td>`mdb_tmdb`</td><td>Use TMDb Rating through MDbList</td></tr><tr><td>`mdb_letterboxd`</td><td>Use Letterboxd Rating through MDbList</td></tr><tr><td>`anidb_rating`</td><td>Use AniDB Rating</td></tr><tr><td>`anidb_average`</td><td>Use AniDB Average</td></tr></table> | +| `mass_imdb_parental_labels` | Updates every item's labels in the library to match the IMDb Parental Guide<br>**Values** `with_none` or `without_none` | | `mass_trakt_rating_update` | Updates every movie/show's user rating in the library to match your custom rating on Trakt if there is one<br>**Values:** `true` or `false` | | `mass_collection_mode` | Updates every Collection in your library to the specified Collection Mode<br>**Values:** `default`: Library default<br>`hide`: Hide Collection<br>`hide_items`: Hide Items in this Collection<br>`show_items`: Show this Collection and its Items<table class="clearTable"><tr><td>`default`</td><td>Library default</td></tr><tr><td>`hide`</td><td>Hide Collection</td></tr><tr><td>`hide_items`</td><td>Hide Items in this Collection</td></tr><tr><td>`show_items`</td><td>Show this Collection and its Items</td></tr></table> | | `update_blank_track_titles ` | Search though every track in a music library and replace any blank track titles with the tracks sort title<br>**Values:** `true` or `false` | diff --git a/modules/cache.py b/modules/cache.py index d78b07d0b..b7e8772a6 100644 --- a/modules/cache.py +++ b/modules/cache.py @@ -203,6 +203,17 @@ def __init__(self, config_path, expiration): media_id TEXT, media_type TEXT)""" ) + cursor.execute( + """CREATE TABLE IF NOT EXISTS imdb_parental ( + key INTEGER PRIMARY KEY, + imdb_id TEXT, + nudity TEXT, + violence TEXT, + profanity TEXT, + alcohol TEXT, + frightening TEXT, + expiration_date TEXT)""" + ) cursor.execute("SELECT count(name) FROM sqlite_master WHERE type='table' AND name='image_map'") if cursor.fetchone()[0] > 0: cursor.execute(f"SELECT DISTINCT library FROM image_map") @@ -695,3 +706,33 @@ def delete_list_ids(self, list_key): connection.row_factory = sqlite3.Row with closing(connection.cursor()) as cursor: cursor.execute(f"DELETE FROM list_ids WHERE list_key = ?", (list_key,)) + + def query_imdb_parental(self, imdb_id, expiration): + imdb_dict = {} + expired = None + with sqlite3.connect(self.cache_path) as connection: + connection.row_factory = sqlite3.Row + with closing(connection.cursor()) as cursor: + cursor.execute("SELECT * FROM imdb_parental WHERE imdb_id = ?", (imdb_id,)) + row = cursor.fetchone() + if row: + imdb_dict["nudity"] = row["nudity"] if row["nudity"] else "None" + imdb_dict["violence"] = row["violence"] if row["violence"] else "None" + imdb_dict["profanity"] = row["profanity"] if row["profanity"] else "None" + imdb_dict["alcohol"] = row["alcohol"] if row["alcohol"] else "None" + imdb_dict["frightening"] = row["frightening"] if row["frightening"] else "None" + datetime_object = datetime.strptime(row["expiration_date"], "%Y-%m-%d") + time_between_insertion = datetime.now() - datetime_object + expired = time_between_insertion.days > expiration + return imdb_dict, expired + + def update_imdb_parental(self, expired, imdb_id, parental, expiration): + expiration_date = datetime.now() if expired is True else (datetime.now() - timedelta(days=random.randint(1, expiration))) + with sqlite3.connect(self.cache_path) as connection: + connection.row_factory = sqlite3.Row + with closing(connection.cursor()) as cursor: + cursor.execute("INSERT OR IGNORE INTO imdb_parental(imdb_id) VALUES(?)", (imdb_id,)) + update_sql = "UPDATE imdb_parental SET nudity = ?, violence = ?, profanity = ?, alcohol = ?, " \ + "frightening = ?, expiration_date = ? WHERE imdb_id = ?" + cursor.execute(update_sql, (parental["nudity"], parental["violence"], parental["profanity"], parental["alcohol"], + parental["frightening"], expiration_date.strftime("%Y-%m-%d"), imdb_id)) \ No newline at end of file diff --git a/modules/config.py b/modules/config.py index a19e1366e..b6fe822d9 100644 --- a/modules/config.py +++ b/modules/config.py @@ -34,6 +34,7 @@ mass_genre_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb", "tvdb": "Use TVDb Metadata", "anidb": "Use AniDB Tag Metadata"} mass_content_options = {"omdb": "Use IMDb Metadata through OMDb", "mdb": "Use MdbList Metadata", "mdb_commonsense": "Use Commonsense Rating through MDbList"} mass_available_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb", "mdb": "Use MdbList Metadata", "tvdb": "Use TVDb Metadata", "anidb": "Use AniDB Metadata"} +imdb_label_options = {"with_none": "Add IMDb Parental Labels including None", "without_none": "Add IMDb Parental Labels including None"} mass_rating_options = { "tmdb": "Use TMDb Rating", "omdb": "Use IMDb Rating through OMDb", @@ -604,7 +605,8 @@ def check_dict(attr): "genre_collections": None, "update_blank_track_titles": None, "mass_content_rating_update": None, - "mass_originally_available_update": None + "mass_originally_available_update": None, + "mass_imdb_parental_labels": None } display_name = f"{params['name']} ({params['mapping_name']})" if lib and "library_name" in lib and lib["library_name"] else params["mapping_name"] @@ -675,6 +677,8 @@ def check_dict(attr): params["mass_content_rating_update"] = check_for_attribute(lib["operations"], "mass_content_rating_update", test_list=mass_content_options, default_is_none=True, save=False) if "mass_originally_available_update" in lib["operations"]: params["mass_originally_available_update"] = check_for_attribute(lib["operations"], "mass_originally_available_update", test_list=mass_available_options, default_is_none=True, save=False) + if "mass_imdb_parental_labels" in lib["operations"]: + params["mass_imdb_parental_labels"] = check_for_attribute(lib["operations"], "mass_imdb_parental_labels", test_list=imdb_label_options, default_is_none=True, save=False) if "mass_trakt_rating_update" in lib["operations"]: params["mass_trakt_rating_update"] = check_for_attribute(lib["operations"], "mass_trakt_rating_update", var_type="bool", default=False, save=False) if "split_duplicates" in lib["operations"]: diff --git a/modules/imdb.py b/modules/imdb.py index 39e8dbd30..2fb737500 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -128,6 +128,24 @@ def _ids_from_url(self, imdb_url, language, limit): return imdb_ids raise Failed(f"IMDb Error: No IMDb IDs Found at {imdb_url}") + def parental_guide(self, imdb_id, ignore_cache=False): + parental_dict = {} + expired = None + if self.config.Cache and not ignore_cache: + parental_dict, expired = self.config.Cache.query_imdb_parental(imdb_id, self.config.expiration) + if parental_dict and expired is False: + return parental_dict + response = self.config.get_html(f"https://www.imdb.com/title/{imdb_id}/parentalguide") + for ptype in ["nudity", "violence", "profanity", "alcohol", "frightening"]: + results = response.xpath(f"//section[@id='advisory-{ptype}']//span[contains(@class,'ipl-status-pill')]/text()") + if results: + parental_dict[ptype] = results[0].strip() + else: + raise Failed(f"IMDb Error: No Item Found for IMDb ID: {imdb_id}") + if self.config.Cache and not ignore_cache: + self.config.Cache.update_imdb_parental(expired, imdb_id, parental_dict, self.config.expiration) + return parental_dict + def _ids_from_chart(self, chart): if chart == "box_office": url = "chart/boxoffice" diff --git a/modules/library.py b/modules/library.py index d0c41f425..a4be913d3 100644 --- a/modules/library.py +++ b/modules/library.py @@ -73,6 +73,7 @@ def __init__(self, config, params): self.mass_critic_rating_update = params["mass_critic_rating_update"] self.mass_content_rating_update = params["mass_content_rating_update"] self.mass_originally_available_update = params["mass_originally_available_update"] + self.mass_imdb_parental_labels = params["mass_imdb_parental_labels"] self.mass_trakt_rating_update = params["mass_trakt_rating_update"] self.radarr_add_all_existing = params["radarr_add_all_existing"] self.radarr_remove_by_tag = params["radarr_remove_by_tag"] @@ -95,7 +96,7 @@ def __init__(self, config, params): self.status = {} self.items_library_operation = True if self.assets_for_all or self.mass_genre_update or self.mass_audience_rating_update \ - or self.mass_critic_rating_update or self.mass_content_rating_update or self.mass_originally_available_update or self.mass_trakt_rating_update \ + or self.mass_critic_rating_update or self.mass_content_rating_update or self.mass_originally_available_update or self.mass_imdb_parental_labels or self.mass_trakt_rating_update \ or self.genre_mapper or self.content_rating_mapper or self.tmdb_collections or self.radarr_add_all_existing or self.sonarr_add_all_existing else False self.library_operation = True if self.items_library_operation or self.delete_unmanaged_collections or self.delete_collections_with_less \ or self.radarr_remove_by_tag or self.sonarr_remove_by_tag or self.mass_collection_mode \ diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 491e0d2bd..dc7603dc9 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -417,6 +417,7 @@ def library_operations(config, library): logger.debug(f"Mass Critic Rating Update: {library.mass_critic_rating_update}") logger.debug(f"Mass Content Rating Update: {library.mass_content_rating_update}") logger.debug(f"Mass Originally Available Update: {library.mass_originally_available_update}") + logger.debug(f"Mass IMDb Parental Labels: {library.mass_imdb_parental_labels}") logger.debug(f"Mass Trakt Rating Update: {library.mass_trakt_rating_update}") logger.debug(f"Mass Collection Mode Update: {library.mass_collection_mode}") logger.debug(f"Split Duplicates: {library.split_duplicates}") @@ -486,6 +487,13 @@ def library_operations(config, library): except Failed: pass + if library.mass_imdb_parental_labels: + try: + parental_guide = config.IMDb.parental_guide(imdb_id) + labels = [f"{k.capitalize()}:{v}" for k, v in parental_guide.items() if library.mass_imdb_parental_labels == "with_none" or v != "None"] + library.edit_tags("label", item, append_tags=labels) + except Failed: + pass path = os.path.dirname(str(item.locations[0])) if library.is_movie else str(item.locations[0]) if library.Radarr and library.radarr_add_all_existing and tmdb_id: path = path.replace(library.Radarr.plex_path, library.Radarr.radarr_path) From 60168b0204667beb82926f204c16bd9c43882e08 Mon Sep 17 00:00:00 2001 From: meisnate12 <meisnate12@gmail.com> Date: Tue, 15 Mar 2022 16:53:37 -0400 Subject: [PATCH 29/32] [26] fix smart_filter --- VERSION | 2 +- plex_meta_manager.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/VERSION b/VERSION index 10599f2a4..3d56d4cd0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop25 +1.16.1-develop26 diff --git a/plex_meta_manager.py b/plex_meta_manager.py index dc7603dc9..bf1637b41 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -935,7 +935,7 @@ def run_collection(config, library, metadata, requested_collections): valid = True if builder.build_collection and ( (builder.smart_url and len(library.get_filter_items(builder.smart_url)) < builder.minimum) - or (len(builder.added_items) + builder.beginning_count < builder.minimum) + or (not builder.smart_url and len(builder.added_items) + builder.beginning_count < builder.minimum) ): logger.info("") logger.info(f"Collection Minimum: {builder.minimum} not met for {mapping_name} Collection") From e5e36f823a0c3bb513d6ebd6c07eb04ae2bb60f9 Mon Sep 17 00:00:00 2001 From: salty <salty@salty.dk> Date: Wed, 16 Mar 2022 01:01:13 +0100 Subject: [PATCH 30/32] fixed arm64 build and slimmed down image Credits for the slimming goes to https://github.com/chazlarson --- Dockerfile | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Dockerfile b/Dockerfile index 6362a8f44..34205f8b9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,18 +1,20 @@ FROM python:3.9-slim ENV TINI_VERSION v0.19.0 -ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini -RUN chmod +x /tini +COPY . / RUN echo "**** install system packages ****" \ && apt-get update \ && apt-get upgrade -y --no-install-recommends \ && apt-get install -y tzdata --no-install-recommends \ - && apt-get install -y gcc g++ libxml2-dev libxslt-dev libz-dev -COPY requirements.txt / -RUN echo "**** install python packages ****" \ + && apt-get install -y gcc g++ libxml2-dev libxslt-dev libz-dev wget \ + && wget -O /tini https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-"$(dpkg --print-architecture | awk -F- '{ print $NF }')" \ + && chmod +x /tini \ && pip3 install --no-cache-dir --upgrade --requirement /requirements.txt \ - && apt-get autoremove -y \ + && apt-get --purge autoremove wget gcc g++ libxml2-dev libxslt-dev libz-dev -y \ && apt-get clean \ + && apt-get update \ + && apt-get check \ + && apt-get -f install \ + && apt-get autoclean \ && rm -rf /requirements.txt /tmp/* /var/tmp/* /var/lib/apt/lists/* -COPY . / VOLUME /config ENTRYPOINT ["/tini", "-s", "python3", "plex_meta_manager.py", "--"] From 8071924a2010840ada830de0639984fa4249f69c Mon Sep 17 00:00:00 2001 From: meisnate12 <meisnate12@gmail.com> Date: Wed, 16 Mar 2022 02:19:34 -0400 Subject: [PATCH 31/32] [27] fix anidb genre update --- VERSION | 2 +- modules/cache.py | 6 +-- modules/convert.py | 103 +++++++++++++++++++------------------------ plex_meta_manager.py | 19 +++++--- 4 files changed, 63 insertions(+), 67 deletions(-) diff --git a/VERSION b/VERSION index 3d56d4cd0..2770ff47a 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop26 +1.16.1-develop27 diff --git a/modules/cache.py b/modules/cache.py index b7e8772a6..2b9f173b7 100644 --- a/modules/cache.py +++ b/modules/cache.py @@ -332,7 +332,7 @@ def _update_map(self, map_name, val1_name, val1, val2_name, val2, expired, media sql = f"UPDATE {map_name} SET {val2_name} = ?, expiration_date = ? WHERE {val1_name} = ?" cursor.execute(sql, (val2, expiration_date.strftime("%Y-%m-%d"), val1)) else: - sql = f"UPDATE {map_name} SET {val2_name} = ?, expiration_date = ?{'' if media_type is None else ', media_type = ?'} WHERE {val1_name} = ?" + sql = f"UPDATE {map_name} SET {val2_name} = ?, expiration_date = ?, media_type = ? WHERE {val1_name} = ?" cursor.execute(sql, (val2, expiration_date.strftime("%Y-%m-%d"), media_type, val1)) def query_omdb(self, imdb_id, expiration): @@ -504,8 +504,8 @@ def query_tmdb_show(self, tmdb_id, expiration): tmdb_dict["status"] = row["status"] if row["status"] else None tmdb_dict["type"] = row["type"] if row["type"] else None tmdb_dict["tvdb_id"] = row["tvdb_id"] if row["tvdb_id"] else None - tmdb_dict["countries"] = row["countries"] if row["countries"] else None - tmdb_dict["seasons"] = row["seasons"] if row["seasons"] else None + tmdb_dict["countries"] = row["countries"] if row["countries"] else "" + tmdb_dict["seasons"] = row["seasons"] if row["seasons"] else "" datetime_object = datetime.strptime(row["expiration_date"], "%Y-%m-%d") time_between_insertion = datetime.now() - datetime_object expired = time_between_insertion.days > expiration diff --git a/modules/convert.py b/modules/convert.py index 7dc1ecb54..e1b010468 100644 --- a/modules/convert.py +++ b/modules/convert.py @@ -10,52 +10,39 @@ class Convert: def __init__(self, config): self.config = config - self._loaded = False self._anidb_ids = {} self._mal_to_anidb = {} self._anilist_to_anidb = {} self._anidb_to_imdb = {} self._anidb_to_tvdb = {} + self._imdb_to_anidb = {} + self._tvdb_to_anidb = {} + for anime_id in self.config.get_json(anime_lists_url): + if "anidb_id" in anime_id: + self._anidb_ids[anime_id["anidb_id"]] = anime_id + if "mal_id" in anime_id: + self._mal_to_anidb[int(anime_id["mal_id"])] = int(anime_id["anidb_id"]) + if "anilist_id" in anime_id: + self._anilist_to_anidb[int(anime_id["anilist_id"])] = int(anime_id["anidb_id"]) + if "imdb_id" in anime_id and str(anime_id["imdb_id"]).startswith("tt"): + self._anidb_to_imdb[int(anime_id["anidb_id"])] = util.get_list(anime_id["imdb_id"]) + for im_id in util.get_list(anime_id["imdb_id"]): + self._imdb_to_anidb[im_id] = int(anime_id["anidb_id"]) + if "thetvdb_id" in anime_id: + self._anidb_to_tvdb[int(anime_id["anidb_id"])] = int(anime_id["thetvdb_id"]) + self._tvdb_to_anidb[int(anime_id["thetvdb_id"])] = int(anime_id["anidb_id"]) - @property - def anidb_ids(self): - self._load_anime_conversion() - return self._anidb_ids - - @property - def mal_to_anidb(self): - self._load_anime_conversion() - return self._mal_to_anidb - - @property - def anilist_to_anidb(self): - self._load_anime_conversion() - return self._anilist_to_anidb - - @property - def anidb_to_imdb(self): - self._load_anime_conversion() - return self._anidb_to_imdb - - @property - def anidb_to_tvdb(self): - self._load_anime_conversion() - return self._anidb_to_tvdb + def imdb_to_anidb(self, imdb_id): + if imdb_id in self._imdb_to_anidb: + return self._imdb_to_anidb[imdb_id] + else: + raise Failed(f"AniDB ID not found for IMDb ID: {imdb_id}") - def _load_anime_conversion(self): - if not self._loaded: - for anime_id in self.config.get_json(anime_lists_url): - if "anidb_id" in anime_id: - self._anidb_ids[anime_id["anidb_id"]] = anime_id - if "mal_id" in anime_id: - self._mal_to_anidb[int(anime_id["mal_id"])] = int(anime_id["anidb_id"]) - if "anilist_id" in anime_id: - self._anilist_to_anidb[int(anime_id["anilist_id"])] = int(anime_id["anidb_id"]) - if "imdb_id" in anime_id and str(anime_id["imdb_id"]).startswith("tt"): - self._anidb_to_imdb[int(anime_id["anidb_id"])] = util.get_list(anime_id["imdb_id"]) - if "thetvdb_id" in anime_id: - self._anidb_to_tvdb[int(anime_id["anidb_id"])] = int(anime_id["thetvdb_id"]) - self._loaded = True + def tvdb_to_anidb(self, tvdb_id): + if int(tvdb_id) in self._tvdb_to_anidb: + return self._tvdb_to_anidb[int(tvdb_id)] + else: + raise Failed(f"AniDB ID not found for TVDb ID: {tvdb_id}") def anidb_to_ids(self, anidb_ids, library): ids = [] @@ -63,18 +50,18 @@ def anidb_to_ids(self, anidb_ids, library): for anidb_id in anidb_list: if anidb_id in library.anidb_map: ids.append((library.anidb_map[anidb_id], "ratingKey")) - elif anidb_id in self.anidb_to_imdb: + elif anidb_id in self._anidb_to_imdb: added = False - for imdb in self.anidb_to_imdb[anidb_id]: + for imdb in self._anidb_to_imdb[anidb_id]: tmdb, tmdb_type = self.imdb_to_tmdb(imdb) if tmdb and tmdb_type == "movie": ids.append((tmdb, "tmdb")) added = True - if added is False and anidb_id in self.anidb_to_tvdb: - ids.append((self.anidb_to_tvdb[anidb_id], "tvdb")) - elif anidb_id in self.anidb_to_tvdb: - ids.append((self.anidb_to_tvdb[anidb_id], "tvdb")) - elif anidb_id in self.anidb_ids: + if added is False and anidb_id in self._anidb_to_tvdb: + ids.append((self._anidb_to_tvdb[anidb_id], "tvdb")) + elif anidb_id in self._anidb_to_tvdb: + ids.append((self._anidb_to_tvdb[anidb_id], "tvdb")) + elif anidb_id in self._anidb_ids: logger.warning(f"Convert Error: No TVDb ID or IMDb ID found for AniDB ID: {anidb_id}") else: logger.warning(f"Convert Error: AniDB ID: {anidb_id} not found") @@ -83,8 +70,8 @@ def anidb_to_ids(self, anidb_ids, library): def anilist_to_ids(self, anilist_ids, library): anidb_ids = [] for anilist_id in anilist_ids: - if anilist_id in self.anilist_to_anidb: - anidb_ids.append(self.anilist_to_anidb[anilist_id]) + if anilist_id in self._anilist_to_anidb: + anidb_ids.append(self._anilist_to_anidb[anilist_id]) else: logger.warning(f"Convert Error: AniDB ID not found for AniList ID: {anilist_id}") return self.anidb_to_ids(anidb_ids, library) @@ -94,8 +81,8 @@ def myanimelist_to_ids(self, mal_ids, library): for mal_id in mal_ids: if int(mal_id) in library.mal_map: ids.append((library.mal_map[int(mal_id)], "ratingKey")) - elif int(mal_id) in self.mal_to_anidb: - ids.extend(self.anidb_to_ids(self.mal_to_anidb[int(mal_id)], library)) + elif int(mal_id) in self._mal_to_anidb: + ids.extend(self.anidb_to_ids(self._mal_to_anidb[int(mal_id)], library)) else: logger.warning(f"Convert Error: AniDB ID not found for MyAnimeList ID: {mal_id}") return ids @@ -275,26 +262,26 @@ def get_id(self, item, library): raise Failed(f"Hama Agent ID: {check_id} not supported") elif item_type == "myanimelist": library.mal_map[int(check_id)] = item.ratingKey - if int(check_id) in self.mal_to_anidb: - anidb_id = self.mal_to_anidb[int(check_id)] + if int(check_id) in self._mal_to_anidb: + anidb_id = self._mal_to_anidb[int(check_id)] else: raise Failed(f"AniDB ID not found for MyAnimeList ID: {check_id}") elif item_type == "local": raise Failed("No match in Plex") else: raise Failed(f"Agent {item_type} not supported") if anidb_id: - if anidb_id in self.anidb_to_imdb: + if anidb_id in self._anidb_to_imdb: added = False - for imdb in self.anidb_to_imdb[anidb_id]: + for imdb in self._anidb_to_imdb[anidb_id]: tmdb, tmdb_type = self.imdb_to_tmdb(imdb) if tmdb and tmdb_type == "movie": imdb_id.append(imdb) tmdb_id.append(tmdb) added = True - if added is False and anidb_id in self.anidb_to_tvdb: - tvdb_id.append(self.anidb_to_tvdb[anidb_id]) - elif anidb_id in self.anidb_to_tvdb: - tvdb_id.append(self.anidb_to_tvdb[anidb_id]) + if added is False and anidb_id in self._anidb_to_tvdb: + tvdb_id.append(self._anidb_to_tvdb[anidb_id]) + elif anidb_id in self._anidb_to_tvdb: + tvdb_id.append(self._anidb_to_tvdb[anidb_id]) else: raise Failed(f"AniDB: {anidb_id} not found") else: diff --git a/plex_meta_manager.py b/plex_meta_manager.py index bf1637b41..c29f925ee 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -459,8 +459,9 @@ def library_operations(config, library): trakt_ratings = config.Trakt.user_ratings(library.is_movie) if library.mass_trakt_rating_update else [] reverse_anidb = {} - for k, v in library.anidb_map.values(): - reverse_anidb[v] = k + if library.mass_genre_update == "anidb": + for k, v in library.anidb_map.values(): + reverse_anidb[v] = k for i, item in enumerate(items, 1): try: @@ -494,6 +495,7 @@ def library_operations(config, library): library.edit_tags("label", item, append_tags=labels) except Failed: pass + path = os.path.dirname(str(item.locations[0])) if library.is_movie else str(item.locations[0]) if library.Radarr and library.radarr_add_all_existing and tmdb_id: path = path.replace(library.Radarr.plex_path, library.Radarr.radarr_path) @@ -542,12 +544,19 @@ def library_operations(config, library): anidb_item = None if library.mass_genre_update == "anidb": if item.ratingKey in reverse_anidb: + anidb_id = reverse_anidb[item.ratingKey] + elif tvdb_id in config.Convert._tvdb_to_anidb: + anidb_id = config.Convert._tvdb_to_anidb[tvdb_id] + elif imdb_id in config.Convert._imdb_to_anidb: + anidb_id = config.Convert._imdb_to_anidb[imdb_id] + else: + anidb_id = None + logger.info(f"{item.title[:25]:<25} | No AniDB ID for Guid: {item.guid}") + if anidb_id: try: - anidb_item = config.AniDB.get_anime(reverse_anidb[item.ratingKey]) + anidb_item = config.AniDB.get_anime(anidb_id) except Failed as e: logger.error(str(e)) - else: - logger.info(f"{item.title[:25]:<25} | No AniDB ID for Guid: {item.guid}") mdb_item = None if library.mass_audience_rating_update in util.mdb_types or library.mass_critic_rating_update in util.mdb_types \ From a9eff11451d32e59831f95b8ec01cf10295698ff Mon Sep 17 00:00:00 2001 From: meisnate12 <meisnate12@gmail.com> Date: Wed, 16 Mar 2022 03:00:22 -0400 Subject: [PATCH 32/32] finishing touches and version --- .github/workflows/release.yml | 9 +++------ VERSION | 2 +- docs/config/operations.md | 6 +++--- docs/config/tmdb.md | 9 +++++---- modules/builder.py | 2 +- 5 files changed, 13 insertions(+), 15 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 88d668d86..681cff275 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,11 +11,8 @@ jobs: runs-on: ubuntu-latest steps: - - name: Check Out Repo - uses: actions/checkout@v3 - - name: Send Discord Release Notification - uses: bythope/discord-webhook-messages@v1.1.0 + uses: nhevia/discord-styled-releases@main with: - webhookUrl: ${{ secrets.RELEASE_WEBHOOK }} - handler: 'release' \ No newline at end of file + webhook_id: ${{ secrets.RELEASE_WEBHOOK_ID }} + webhook_token: ${{ secrets.RELEASE_WEBHOOK_TOKEN }} \ No newline at end of file diff --git a/VERSION b/VERSION index 2770ff47a..4a02d2c31 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.16.1-develop27 +1.16.2 diff --git a/docs/config/operations.md b/docs/config/operations.md index 86f58f7af..f098e2028 100644 --- a/docs/config/operations.md +++ b/docs/config/operations.md @@ -34,9 +34,9 @@ The available attributes for the operations attribute are as follows | `radarr_remove_by_tag` | Removes every item from Radarr with the Tags given<br>**Values:** List or comma separated string of tags | | `sonarr_add_all` | Adds every item in the library to Sonarr. The existing paths in plex will be used as the root folder of each item, if the paths in Plex are not the same as your Sonarr paths you can use the `plex_path` and `sonarr_path` [Sonarr](sonarr) details to convert the paths.<br>**Values:** `true` or `false` | | `sonarr_remove_by_tag` | Removes every item from Sonarr with the Tags given<br>**Values:** List or comma separated string of tags | -| `genre_mapper` | Allows genres to be changed to other genres or be removed from every item in your library.<br>**Values:** [see below for usage](#genre-mapper) | -| `content_rating_mapper` | Allows content ratings to be changed to other content ratings or be removed from every item in your library.<br>**Values:** [see below for usage](#content-rating-mapper) | -| `metadata_backup` | Creates/Maintains a PMM [Metadata File](../metadata/metadata) with a full `metadata` mapping based on the library's items locked attributes.<br>**Values:** [see below for usage](#metadata-backup) | +| [`genre_mapper`](#genre-mapper) | Allows genres to be changed to other genres or be removed from every item in your library.<br>**Values:** [see below for usage](#genre-mapper) | +| [`content_rating_mapper`](#content-rating-mapper) | Allows content ratings to be changed to other content ratings or be removed from every item in your library.<br>**Values:** [see below for usage](#content-rating-mapper) | +| [`metadata_backup`](#metadata-backup) | Creates/Maintains a PMM [Metadata File](../metadata/metadata) with a full `metadata` mapping based on the library's items locked attributes.<br>**Values:** [see below for usage](#metadata-backup) | ## Genre Mapper diff --git a/docs/config/tmdb.md b/docs/config/tmdb.md index 4864a79fc..4acc06d25 100644 --- a/docs/config/tmdb.md +++ b/docs/config/tmdb.md @@ -11,9 +11,10 @@ tmdb: language: en ``` -| Attribute | Allowed Values | Default | Required | -|:-----------|:---------------------|:-------:|:--------:| -| `apikey` | User TMDb V3 API Key | N/A | ✅ | -| `language` | User Language | en | ❌ | +| Attribute | Allowed Values | Default | Required | +|:-------------------|:--------------------------------------------------------------------------|:-------:|:--------:| +| `apikey` | User TMDb V3 API Key | N/A | ✅ | +| `language` | User Language | en | ❌ | +| `cache_expiration` | Number of days before each cache mapping expires and has to be re-cached. | 60 | ❌ | If you do not have a TMDb V3 API key please refer to this [guide](https://developers.themoviedb.org/3/getting-started/introduction). diff --git a/modules/builder.py b/modules/builder.py index d8fe56f02..32e6f40e7 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -600,7 +600,7 @@ def __init__(self, config, metadata, name, no_missing, data, library=None): raise Failed(f"{self.Type} Error: {method_final} attribute only allowed for album collections") elif not self.library.is_music and method_name in music_only_builders: raise Failed(f"{self.Type} Error: {method_final} attribute only allowed for music libraries") - elif self.collection_level != "episode" and method_name in episode_parts_only: + elif not self.playlist and self.collection_level != "episode" and method_name in episode_parts_only: raise Failed(f"{self.Type} Error: {method_final} attribute only allowed with Collection Level: episode") elif self.parts_collection and method_name not in parts_collection_valid: raise Failed(f"{self.Type} Error: {method_final} attribute not allowed with Collection Level: {self.collection_level.capitalize()}")