From d4749eda9b88880b9ce155e9c0a180a39affb3e7 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Sun, 6 Feb 2022 02:33:09 -0500 Subject: [PATCH] add mdblist mass rating update options --- VERSION | 2 +- modules/cache.py | 71 ++++++++++++++++++++++++++++++++++ modules/config.py | 44 ++++++++++++++++++--- modules/mdblist.py | 91 ++++++++++++++++++++++++++++++++++++++++++++ modules/util.py | 8 +++- plex_meta_manager.py | 72 +++++++++++++++++++++++++---------- 6 files changed, 260 insertions(+), 28 deletions(-) diff --git a/VERSION b/VERSION index 838bdac0..afa2532f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.15.1-develop50 +1.15.1-develop51 diff --git a/modules/cache.py b/modules/cache.py index 57e23f05..a6f5508c 100644 --- a/modules/cache.py +++ b/modules/cache.py @@ -86,6 +86,28 @@ class Cache: episode_num INTEGER, expiration_date TEXT)""" ) + cursor.execute( + """CREATE TABLE IF NOT EXISTS mdb_data ( + key INTEGER PRIMARY KEY, + key_id TEXT UNIQUE, + title TEXT, + year INTEGER, + type TEXT, + imdbid TEXT, + traktid INTEGER, + tmdbid INTEGER, + score INTEGER, + imdb_rating REAL, + metacritic_rating INTEGER, + metacriticuser_rating REAL, + trakt_rating INTEGER, + tomatoes_rating INTEGER, + tomatoesaudience_rating INTEGER, + tmdb_rating INTEGER, + letterboxd_rating REAL, + commonsense TEXT, + expiration_date TEXT)""" + ) cursor.execute( """CREATE TABLE IF NOT EXISTS anime_map ( key INTEGER PRIMARY KEY, @@ -288,6 +310,55 @@ class Cache: omdb.series_id, omdb.season_num, omdb.episode_num, expiration_date.strftime("%Y-%m-%d"), omdb.imdb_id)) + def query_mdb(self, key_id): + mdb_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 mdb_data 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["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 + mdb_dict["tmdbid"] = row["tmdbid"] if row["tmdbid"] else None + mdb_dict["score"] = row["score"] if row["score"] else None + mdb_dict["commonsense"] = row["commonsense"] if row["commonsense"] else None + mdb_dict["ratings"] = [ + {"source": "imdb", "value": row["imdb_rating"] if row["imdb_rating"] else None}, + {"source": "metacritic", "value": row["metacritic_rating"] if row["metacritic_rating"] else None}, + {"source": "metacriticuser", "value": row["metacriticuser_rating"] if row["metacriticuser_rating"] else None}, + {"source": "trakt", "value": row["trakt_rating"] if row["trakt_rating"] else None}, + {"source": "tomatoes", "value": row["tomatoes_rating"] if row["tomatoes_rating"] else None}, + {"source": "tomatoesaudience", "value": row["tomatoesaudience_rating"] if row["tomatoesaudience_rating"] else None}, + {"source": "tmdb", "value": row["tmdb_rating"] if row["tmdb_rating"] else None}, + {"source": "letterboxd", "value": row["letterboxd_rating"] if row["letterboxd_rating"] else None} + ] + datetime_object = datetime.strptime(row["expiration_date"], "%Y-%m-%d") + time_between_insertion = datetime.now() - datetime_object + expired = time_between_insertion.days > self.expiration + return mdb_dict, expired + + def update_mdb(self, expired, key_id, mdb): + expiration_date = datetime.now() if expired is True else (datetime.now() - timedelta(days=random.randint(1, self.expiration))) + with sqlite3.connect(self.cache_path) as connection: + connection.row_factory = sqlite3.Row + with closing(connection.cursor()) as cursor: + cursor.execute("INSERT OR IGNORE INTO mdb_data(key_id) VALUES(?)", (key_id,)) + update_sql = "UPDATE mdb_data SET title = ?, year = ?, type = ?, imdbid = ?, traktid = ?, " \ + "tmdbid = ?, score = ?, imdb_rating = ?, metacritic_rating = ?, metacriticuser_rating = ?, " \ + "trakt_rating = ?, tomatoes_rating = ?, tomatoesaudience_rating = ?, tmdb_rating = ?, " \ + "letterboxd_rating = ?, 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.commonsense, + expiration_date.strftime("%Y-%m-%d"), key_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 7da26de1..b46a3305 100644 --- a/modules/config.py +++ b/modules/config.py @@ -32,6 +32,19 @@ logger = logging.getLogger("Plex Meta Manager") 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_rating_options = { + "tmdb": "Use TMDb Rating", + "omdb": "Use IMDb Rating through OMDb", + "mdb": "Use MdbList Average Score", + "mdb_imdb": "Use IMDb Rating through MDbList", + "mdb_metacritic": "Use Metacritic Rating through MDbList", + "mdb_metacriticuser": "Use Metacritic User Rating through MDbList", + "mdb_trakt": "Use Trakt Rating through MDbList", + "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" +} class ConfigFile: def __init__(self, default_dir, attrs, read_only): @@ -149,6 +162,7 @@ class ConfigFile: if "tmdb" in new_config: new_config["tmdb"] = new_config.pop("tmdb") if "tautulli" in new_config: new_config["tautulli"] = new_config.pop("tautulli") if "omdb" in new_config: new_config["omdb"] = new_config.pop("omdb") + if "mdblist" in new_config: new_config["mdblist"] = new_config.pop("mdblist") if "notifiarr" in new_config: new_config["notifiarr"] = new_config.pop("notifiarr") if "anidb" in new_config: new_config["anidb"] = new_config.pop("anidb") if "radarr" in new_config: @@ -372,6 +386,21 @@ class ConfigFile: util.separator() + self.Mdblist = Mdblist(self) + if "mdblist" in self.data: + logger.info("Connecting to Mdblist...") + try: + self.Mdblist.add_key(check_for_attribute(self.data, "apikey", parent="mdblist", throw=True)) + logger.info("Mdblist Connection Successful") + except Failed as e: + self.errors.append(e) + logger.error(e) + logger.info("Mdblist Connection Failed") + else: + logger.warning("mdblist attribute not found") + + util.separator() + self.Trakt = None if "trakt" in self.data: logger.info("Connecting to Trakt...") @@ -492,7 +521,6 @@ class ConfigFile: self.ICheckMovies = ICheckMovies(self) self.Letterboxd = Letterboxd(self) self.StevenLu = StevenLu(self) - self.Mdblist = Mdblist(self) util.separator() @@ -605,8 +633,8 @@ class ConfigFile: 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_audience_rating_update"] = check_for_attribute(lib, "mass_audience_rating_update", test_list=mass_update_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_update_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) params["split_duplicates"] = check_for_attribute(lib, "split_duplicates", var_type="bool", default=False, save=False, do_print=False) params["radarr_add_all_existing"] = check_for_attribute(lib, "radarr_add_all_existing", var_type="bool", default=False, save=False, do_print=False) @@ -624,9 +652,9 @@ class ConfigFile: 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) 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_update_options, default_is_none=True, save=False) + 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"]: - params["mass_critic_rating_update"] = check_for_attribute(lib["operations"], "mass_critic_rating_update", test_list=mass_update_options, default_is_none=True, save=False) + 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_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"]: @@ -724,8 +752,8 @@ class ConfigFile: logger.error("Config Error: operations must be a dictionary") def error_check(attr, service): + err = f"Config Error: {attr} cannot be {params[attr]} without a successful {service} Connection" params[attr] = None - err = f"Config Error: {attr} cannot be omdb without a successful {service} Connection" self.errors.append(err) logger.error(err) @@ -735,6 +763,10 @@ class ConfigFile: 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 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 self.Trakt is None and params["mass_trakt_rating_update"]: error_check("mass_trakt_rating_update", "Trakt") diff --git a/modules/mdblist.py b/modules/mdblist.py index 5e12e071..dbef0b34 100644 --- a/modules/mdblist.py +++ b/modules/mdblist.py @@ -8,12 +8,103 @@ logger = logging.getLogger("Plex Meta Manager") builders = ["mdblist_list"] list_sorts = ["score", "released", "updated", "imdbrating", "rogerebert", "imdbvotes", "budget", "revenue"] base_url = "https://mdblist.com/lists" +api_url = "https://mdblist.com/api/" headers = {"User-Agent": "Plex-Meta-Manager"} +class MDbObj: + def __init__(self, data): + self._data = data + self.title = data["title"] + self.year = util.check_num(data["year"]) + self.type = data["type"] + self.imdbid = data["imdbid"] + self.traktid = util.check_num(data["traktid"]) + self.tmdbid = util.check_num(data["tmdbid"]) + self.score = util.check_num(data["score"]) + self.imdb_rating = None + self.metacritic_rating = None + self.metacriticuser_rating = None + self.trakt_rating = None + self.tomatoes_rating = None + self.tomatoesaudience_rating = None + self.tmdb_rating = None + self.letterboxd_rating = None + for rating in data["ratings"]: + if rating["source"] == "imdb": + self.imdb_rating = util.check_num(rating["value"], is_int=False) + elif rating["source"] == "metacritic": + self.metacritic_rating = util.check_num(rating["value"]) + elif rating["source"] == "metacriticuser": + self.metacriticuser_rating = util.check_num(rating["value"], is_int=False) + elif rating["source"] == "trakt": + self.trakt_rating = util.check_num(rating["value"]) + elif rating["source"] == "tomatoes": + self.tomatoes_rating = util.check_num(rating["value"]) + elif rating["source"] == "tomatoesaudience": + self.tomatoesaudience_rating = util.check_num(rating["value"]) + elif rating["source"] == "tmdb": + self.tmdb_rating = util.check_num(rating["value"]) + elif rating["source"] == "letterboxd": + self.letterboxd_rating = util.check_num(rating["value"], is_int=False) + self.commonsense = data["commonsense"] + + class Mdblist: def __init__(self, config): self.config = config + self.apikey = None + self.limit = False + + def add_key(self, apikey): + self.apikey = apikey + try: + self._request(imdb_id="tt0080684", ignore_cache=True) + except Failed: + self.apikey = None + raise + + @property + def has_key(self): + return self.apikey is not None + + def _request(self, imdb_id=None, tmdb_id=None, is_movie=True, ignore_cache=False): + params = {"apikey": self.apikey} + if imdb_id: + params["i"] = imdb_id + key = imdb_id + elif tmdb_id: + params["tm"] = tmdb_id + params["m"] = "movie" if is_movie else "show" + key = f"{'tm' if is_movie else 'ts'}{tmdb_id}" + else: + raise Failed("MdbList Error: Either IMDb ID or TMDb ID and TMDb Type Required") + expired = None + if self.config.Cache and not ignore_cache: + mdb_dict, expired = self.config.Cache.query_mdb(key) + if mdb_dict and expired is False: + return MDbObj(mdb_dict) + if self.config.trace_mode: + logger.debug(f"ID: {key}") + response = self.config.get_json(api_url, params=params) + if "response" in response and response["response"] is False: + if response["error"] == "API Limit Reached!": + self.limit = True + raise Failed(f"MdbList Error: {response['error']}") + else: + mdb = MDbObj(response) + if self.config.Cache and not ignore_cache: + self.config.Cache.update_mdb(expired, key, mdb) + return mdb + + def get_imdb(self, imdb_id): + return self._request(imdb_id=imdb_id) + + def get_series(self, tmdb_id): + return self._request(tmdb_id=tmdb_id, is_movie=False) + + def get_movie(self, tmdb_id): + return self._request(tmdb_id=tmdb_id, is_movie=True) def validate_mdblist_lists(self, mdb_lists): valid_lists = [] diff --git a/modules/util.py b/modules/util.py index 1e2be6cb..f347cd3b 100644 --- a/modules/util.py +++ b/modules/util.py @@ -81,12 +81,12 @@ advance_tags_to_edit = { "metadata_language", "use_original_title"], "Artist": ["album_sorting"] } - tags_to_edit = { "Movie": ["genre", "label", "collection", "country", "director", "producer", "writer"], "Show": ["genre", "label", "collection"], "Artist": ["genre", "style", "mood", "country", "collection", "similar_artist"] } +mdb_types = ["mdb", "mdb_imdb", "mdb_metacritic", "mdb_metacriticuser", "mdb_trakt", "mdb_tomatoes", "mdb_tomatoesaudience", "mdb_tmdb", "mdb_letterboxd"] def tab_new_lines(data): return str(data).replace("\n", "\n ") if "\n" in str(data) else str(data) @@ -323,6 +323,12 @@ def time_window(tw): else: return tw +def check_num(num, is_int=True): + try: + return int(str(num)) if is_int else float(str(num)) + except (ValueError, TypeError): + return None + def glob_filter(filter_in): filter_in = filter_in.translate({ord("["): "[[]", ord("]"): "[]]"}) if "[" in filter_in else filter_in return glob.glob(filter_in) diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 0b6a9190..92f95724 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -536,9 +536,53 @@ def library_operations(config, library): else: logger.info(util.adjust_space(f"{item.title[:25]:<25} | No TVDb 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: + if config.Mdblist.limit is False: + if tmdb_id and not imdb_id: + imdb_id = config.Convert.tmdb_to_imdb(tmdb_id) + elif tvdb_id and not imdb_id: + imdb_id = config.Convert.tvdb_to_imdb(tvdb_id) + if imdb_id: + try: + mdb_item = config.Mdblist.get_imdb(imdb_id) + except Failed as e: + logger.error(util.adjust_space(str(e))) + except Exception: + logger.error(f"IMDb ID: {imdb_id}") + raise + else: + logger.info(util.adjust_space(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 + def get_rating(attribute): + if tmdb_item and attribute == "tmdb": + return tmdb_item.vote_average + elif omdb_item and attribute in ["omdb", "imdb"]: + return omdb_item.imdb_rating + elif mdb_item and attribute == "mdb": + return mdb_item.score / 10 if mdb_item.score else None + elif mdb_item and attribute == "mdb_imdb": + return mdb_item.imdb_rating if mdb_item.imdb_rating else None + elif mdb_item and attribute == "mdb_metacritic": + return mdb_item.metacritic_rating / 10 if mdb_item.metacritic_rating else None + elif mdb_item and attribute == "mdb_metacriticuser": + return mdb_item.metacriticuser_rating if mdb_item.metacriticuser_rating else None + elif mdb_item and attribute == "mdb_trakt": + return mdb_item.trakt_rating / 10 if mdb_item.trakt_rating else None + elif mdb_item and attribute == "mdb_tomatoes": + return mdb_item.tmdb_rating / 10 if mdb_item.tomatoes_rating else None + elif mdb_item and attribute == "mdb_tomatoesaudience": + return mdb_item.tomatoesaudience_rating / 10 if mdb_item.tomatoesaudience_rating else None + elif mdb_item and attribute == "mdb_tmdb": + 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 + else: + raise Failed + if library.mass_genre_update: try: if tmdb_item and library.mass_genre_update == "tmdb": @@ -554,34 +598,22 @@ def library_operations(config, library): pass if library.mass_audience_rating_update: try: - if tmdb_item and library.mass_audience_rating_update == "tmdb": - new_rating = tmdb_item.vote_average - elif omdb_item and library.mass_audience_rating_update in ["omdb", "imdb"]: - new_rating = omdb_item.imdb_rating - else: - raise Failed + new_rating = get_rating(library.mass_audience_rating_update) if new_rating is None: logger.info(util.adjust_space(f"{item.title[:25]:<25} | No Rating Found")) - else: - if library.mass_audience_rating_update and str(item.audienceRating) != str(new_rating): - library.edit_query(item, {"audienceRating.value": new_rating, "audienceRating.locked": 1}) - logger.info(util.adjust_space(f"{item.title[:25]:<25} | Audience Rating | {new_rating}")) + elif str(item.audienceRating) != str(new_rating): + library.edit_query(item, {"audienceRating.value": new_rating, "audienceRating.locked": 1}) + logger.info(util.adjust_space(f"{item.title[:25]:<25} | Audience Rating | {new_rating}")) except Failed: pass if library.mass_critic_rating_update: try: - if tmdb_item and library.mass_critic_rating_update == "tmdb": - new_rating = tmdb_item.vote_average - elif omdb_item and library.mass_critic_rating_update in ["omdb", "imdb"]: - new_rating = omdb_item.imdb_rating - else: - raise Failed + new_rating = get_rating(library.mass_critic_rating_update) if new_rating is None: logger.info(util.adjust_space(f"{item.title[:25]:<25} | No Rating Found")) - else: - if library.mass_critic_rating_update and str(item.rating) != str(new_rating): - library.edit_query(item, {"rating.value": new_rating, "rating.locked": 1}) - logger.info(util.adjust_space(f"{item.title[:25]:<25} | Critic Rating | {new_rating}")) + elif str(item.rating) != str(new_rating): + library.edit_query(item, {"rating.value": new_rating, "rating.locked": 1}) + logger.info(util.adjust_space(f"{item.title[:25]:<25} | Critic Rating | {new_rating}")) except Failed: pass if library.genre_mapper: