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 41c11ffb..6ba927a3 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 5cb4e5f4..6f2ad3da 100644 --- a/modules/config.py +++ b/modules/config.py @@ -31,7 +31,7 @@ from ruamel import yaml 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 @@ class ConfigFile: 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 @@ class ConfigFile: 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 6ba927a3..fd6ec9f5 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 045c2a2d..9d44b9cf 100644 --- a/modules/meta.py +++ b/modules/meta.py @@ -268,11 +268,11 @@ class MetadataFile(DataFile): 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 @@ class MetadataFile(DataFile): 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 fd6ec9f5..30d36565 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 9d44b9cf..b5d0c273 100644 --- a/modules/meta.py +++ b/modules/meta.py @@ -284,9 +284,9 @@ class MetadataFile(DataFile): 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 @@ class MetadataFile(DataFile): 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 @@ class MetadataFile(DataFile): 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 @@ class MetadataFile(DataFile): 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 d47ac9ff..34e99e43 100644 --- a/modules/tmdb.py +++ b/modules/tmdb.py @@ -139,7 +139,7 @@ class TMDb: 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 d290013f..51ecd59c 100644 --- a/modules/trakt.py +++ b/modules/trakt.py @@ -241,7 +241,7 @@ class Trakt: 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 f872588e..85e2a83f 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 30d36565..11e9c31f 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 50487fcf..c4486167 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -169,7 +169,7 @@ all_filters = boolean_filters + special_filters + \ 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 11e9c31f..bc2cbf69 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 c4486167..bdcab5da 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -1898,9 +1898,9 @@ class CollectionBuilder: 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 @@ class CollectionBuilder: 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 @@ class CollectionBuilder: 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 f16d9387..6666e828 100644 --- a/modules/cache.py +++ b/modules/cache.py @@ -109,6 +109,56 @@ class Cache: 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 @@ class Cache: 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 6f2ad3da..566ed5fa 100644 --- a/modules/config.py +++ b/modules/config.py @@ -366,7 +366,8 @@ class ConfigFile: 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 b5d0c273..3c87b648 100644 --- a/modules/meta.py +++ b/modules/meta.py @@ -303,8 +303,8 @@ class MetadataFile(DataFile): 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 @@ class MetadataFile(DataFile): 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 @@ class MetadataFile(DataFile): 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 @@ class MetadataFile(DataFile): 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 34e99e43..2544fc76 100644 --- a/modules/tmdb.py +++ b/modules/tmdb.py @@ -57,11 +57,100 @@ discover_movie_sort = [ 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 @@ class TMDb: 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 @@ class TMDb: 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 @@ class TMDb: 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 2b183f91..93a4199a 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 library_operations(config, library): 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 bc2cbf69..fa9c14dc 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 4b4bf150..db3d0878 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 a462af74..8bf25940 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 bdcab5da..45302488 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -2399,10 +2399,10 @@ class CollectionBuilder: 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 566ed5fa..8ead5ebb 100644 --- a/modules/config.py +++ b/modules/config.py @@ -381,7 +381,7 @@ class ConfigFile: 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 @@ class ConfigFile: 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 fa9c14dc..9504e7dc 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 8ead5ebb..9b8a0f4d 100644 --- a/modules/config.py +++ b/modules/config.py @@ -593,6 +593,7 @@ class ConfigFile: "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 @@ class ConfigFile: 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 9ad5e2d0..6e57f27c 100644 --- a/modules/library.py +++ b/modules/library.py @@ -83,6 +83,7 @@ class Library(ABC): 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 @@ class Library(ABC): 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 93a4199a..97777c2b 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 library_operations(config, library): 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 fa9c14dc..9504e7dc 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 8ead5ebb..9b8a0f4d 100644 --- a/modules/config.py +++ b/modules/config.py @@ -593,6 +593,7 @@ class ConfigFile: "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 @@ class ConfigFile: 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 9ad5e2d0..6e57f27c 100644 --- a/modules/library.py +++ b/modules/library.py @@ -83,6 +83,7 @@ class Library(ABC): 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 @@ class Library(ABC): 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 93a4199a..97777c2b 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 library_operations(config, library): 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 9504e7dc..7d01a8df 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 85e2a83f..5906fc79 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 ac571ced..965dbfa5 100644 --- a/modules/anilist.py +++ b/modules/anilist.py @@ -4,8 +4,12 @@ from modules.util import Failed 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 @@ mod_searches = [ 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 @@ class AniList: 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 @@ class AniList: 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 45302488..8c19c79f 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -178,7 +178,7 @@ custom_sort_builders = [ "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 @@ class CollectionBuilder: 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 f316e2d6..4a55b354 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 69bcdfaf..bdf223de 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 4a55b354..e1c78517 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 965dbfa5..68f85f16 100644 --- a/modules/anilist.py +++ b/modules/anilist.py @@ -5,11 +5,11 @@ from modules.util import Failed 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 @@ mod_searches = [ 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 @@ class AniList: 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 @@ class AniList: 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 8c19c79f..129563d0 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -958,11 +958,11 @@ class CollectionBuilder: 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 bdf223de..f0163470 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 54a1cf21..e82db9a4 100644 --- a/modules/tvdb.py +++ b/modules/tvdb.py @@ -66,11 +66,11 @@ class TVDbObj: 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 @@ class TVDbObj: 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 f0163470..63eb2b7c 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 6666e828..07290cd8 100644 --- a/modules/cache.py +++ b/modules/cache.py @@ -22,7 +22,9 @@ class Cache: 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 @@ class Cache: 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 @@ class Cache: 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 @@ class Cache: 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 @@ class Cache: 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 @@ class Cache: 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 @@ class Cache: 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 9b8a0f4d..b1037ae7 100644 --- a/modules/config.py +++ b/modules/config.py @@ -33,6 +33,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_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 @@ class ConfigFile: "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 @@ class ConfigFile: 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 @@ class ConfigFile: 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 6e57f27c..d0c41f42 100644 --- a/modules/library.py +++ b/modules/library.py @@ -72,6 +72,7 @@ class Library(ABC): 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 @@ class Library(ABC): 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 a1333036..23a13460 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 @@ class MDbObj: 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 1cf954cc..7d7a0ca7 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 @@ class OMDbObj: 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 e82db9a4..bde31947 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 @@ class TVDbObj: 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 a835d418..a1ce95bd 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 library_operations(config, library): 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 63eb2b7c..6ffda7fc 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 2544fc76..9be07e74 100644 --- a/modules/tmdb.py +++ b/modules/tmdb.py @@ -110,7 +110,7 @@ class TMDbMovie(TMDBObj): 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 @@ class TMDbShow(TMDBObj): 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 6ffda7fc..ecdaacb8 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 b1037ae7..4289b148 100644 --- a/modules/config.py +++ b/modules/config.py @@ -792,7 +792,7 @@ class ConfigFile: 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 ecdaacb8..8858c6e4 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 a1ce95bd..7cdfca87 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 8858c6e4..d53c5adb 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 3c87b648..9fc21132 100644 --- a/modules/meta.py +++ b/modules/meta.py @@ -246,9 +246,11 @@ class MetadataFile(DataFile): 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 @@ class MetadataFile(DataFile): 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 d53c5adb..202e6f97 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 9fc21132..5060a33e 100644 --- a/modules/meta.py +++ b/modules/meta.py @@ -459,7 +459,7 @@ class MetadataFile(DataFile): 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 202e6f97..98c90fdc 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 07290cd8..82983b50 100644 --- a/modules/cache.py +++ b/modules/cache.py @@ -513,7 +513,7 @@ class Cache: 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 98c90fdc..2de0db69 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 129563d0..eaf58f1c 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -1622,7 +1622,7 @@ class CollectionBuilder: 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 2de0db69..2f6d98f8 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 82983b50..d78b07d0 100644 --- a/modules/cache.py +++ b/modules/cache.py @@ -493,6 +493,8 @@ class Cache: 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 8d10349e..996e9865 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -793,6 +793,7 @@ class Plex(Library): 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 e1bc3c17..0195427b 100644 --- a/modules/sonarr.py +++ b/modules/sonarr.py @@ -110,7 +110,7 @@ class Sonarr: 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 2f6d98f8..73519de3 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 e2674e2c..d845d5eb 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 23118240..a9039693 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 0c0a4539..7b76ffaa 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 @@ urls = { "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 @@ class AniDB: 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 @@ class AniDB: 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 eaf58f1c..d8fe56f0 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -934,7 +934,7 @@ class CollectionBuilder: 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 @@ class CollectionBuilder: 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 4289b148..5afb4db6 100644 --- a/modules/config.py +++ b/modules/config.py @@ -31,9 +31,9 @@ from ruamel import yaml 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 @@ mass_rating_options = { "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 @@ class ConfigFile: 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 7cdfca87..73879294 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 library_operations(config, library): 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 library_operations(config, library): 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 library_operations(config, library): 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 73519de3..6f4702bf 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 73879294..c3f2f3ae 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -584,18 +584,33 @@ def library_operations(config, library): 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 library_operations(config, library): 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 library_operations(config, library): 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 6f4702bf..d7f50325 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 d845d5eb..5b663c5c 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 5afb4db6..a19e1366 100644 --- a/modules/config.py +++ b/modules/config.py @@ -730,30 +730,22 @@ class ConfigFile: 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 68f85f16..757f2475 100644 --- a/modules/anilist.py +++ b/modules/anilist.py @@ -221,7 +221,7 @@ class AniList: 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 d7f50325..1741532c 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 c3f2f3ae..491e0d2b 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -2,7 +2,7 @@ import argparse, os, sys, time, traceback 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 @@ with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), "VERSION")) a 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 1741532c..10599f2a 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 5b663c5c..86f58f7a 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 d78b07d0..b7e8772a 100644 --- a/modules/cache.py +++ b/modules/cache.py @@ -203,6 +203,17 @@ class Cache: 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 @@ class Cache: 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 a19e1366..b6fe822d 100644 --- a/modules/config.py +++ b/modules/config.py @@ -34,6 +34,7 @@ sync_modes = {"append": "Only Add Items to the Collection or Playlist", "sync": 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 @@ class ConfigFile: "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 @@ class ConfigFile: 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 39e8dbd3..2fb73750 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -128,6 +128,24 @@ class IMDb: 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 d0c41f42..a4be913d 100644 --- a/modules/library.py +++ b/modules/library.py @@ -73,6 +73,7 @@ class Library(ABC): 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 @@ class Library(ABC): 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 491e0d2b..dc7603dc 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 10599f2a..3d56d4cd 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 dc7603dc..bf1637b4 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 6362a8f4..34205f8b 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 3d56d4cd..2770ff47 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 b7e8772a..2b9f173b 100644 --- a/modules/cache.py +++ b/modules/cache.py @@ -332,7 +332,7 @@ class Cache: 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 @@ class Cache: 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 7dc1ecb5..e1b01046 100644 --- a/modules/convert.py +++ b/modules/convert.py @@ -10,52 +10,39 @@ anime_lists_url = "https://raw.githubusercontent.com/Fribb/anime-lists/master/an 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 @@ class Convert: 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 @@ class Convert: 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 @@ class Convert: 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 @@ class Convert: 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 bf1637b4..c29f925e 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 88d668d8..681cff27 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 2770ff47..4a02d2c3 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 86f58f7a..f098e202 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 4864a79f..4acc06d2 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 d8fe56f0..32e6f40e 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -600,7 +600,7 @@ class CollectionBuilder: 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()}")