diff --git a/README.md b/README.md index 7f824f3d..dd6930a5 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Plex Meta Manager -#### Version 1.0.3 +#### Version 1.1.0 The original concept for Plex Meta Manager is [Plex Auto Collections](https://github.com/mza921/Plex-Auto-Collections), but this is rewritten from the ground up to be able to include a scheduler, metadata edits, multiple libraries, and logging. Plex Meta Manager is a Python 3 script that can be continuously run using YAML configuration files to update on a schedule the metadata of the movies, shows, and collections in your libraries as well as automatically build collections based on various methods all detailed in the wiki. Some collection examples that the script can automatically build and update daily include Plex Based Searches like actor, genre, or studio collections or Collections based on TMDb, IMDb, Trakt, TVDb, AniDB, or MyAnimeList lists and various other services. @@ -17,6 +17,6 @@ The script is designed to work with most Metadata agents including the new Plex * If you're getting an Error or have an Enhancement post in the [Issues](https://github.com/meisnate12/Plex-Meta-Manager/issues) * If you have a configuration question visit the [Discussions](https://github.com/meisnate12/Plex-Meta-Manager/discussions) -* To see user submited Metadata configuration files and you could even add your own go to the [Plex Meta Manager Configs](https://github.com/meisnate12/Plex-Meta-Manager-Configs) +* To see user submitted Metadata configuration files and you could even add your own go to the [Plex Meta Manager Configs](https://github.com/meisnate12/Plex-Meta-Manager-Configs) * Pull Request are welcome * [Buy Me a Pizza](https://www.buymeacoffee.com/meisnate12) diff --git a/config/config.yml.template b/config/config.yml.template index 1e90c918..18504972 100644 --- a/config/config.yml.template +++ b/config/config.yml.template @@ -13,7 +13,8 @@ plex: # Can be individually specified pe token: #################### sync_mode: append asset_directory: config/assets - show_unmanaged_collections: true + show_unmanaged: true + show_filtered: false radarr: # Can be individually specified per library as well url: http://192.168.1.12:7878 token: ################################ diff --git a/modules/anidb.py b/modules/anidb.py index 318f442f..72fe6c3a 100644 --- a/modules/anidb.py +++ b/modules/anidb.py @@ -23,9 +23,12 @@ class AniDBAPI: def convert_tvdb_to_anidb(self, tvdb_id): return self.convert_anidb(tvdb_id, "tvdbid", "anidbid") def convert_imdb_to_anidb(self, imdb_id): return self.convert_anidb(imdb_id, "imdbid", "anidbid") def convert_anidb(self, input_id, from_id, to_id): - ids = self.id_list.xpath("//anime[@{}='{}']/@{}".format(from_id, input_id, to_id)) + ids = self.id_list.xpath("//anime[contains(@{}, '{}')]/@{}".format(from_id, input_id, to_id)) if len(ids) > 0: - if len(ids[0]) > 0: return ids[0] if to_id == "imdbid" else int(ids[0]) + if from_id == "tvdbid": return [int(id) for id in ids] + if len(ids[0]) > 0: + try: return ids[0].split(",") if to_id == "imdbid" else int(ids[0]) + except ValueError: raise Failed("AniDB Error: No {} ID found for {} ID: {}".format(util.pretty_ids[to_id], util.pretty_ids[from_id], input_id)) else: raise Failed("AniDB Error: No {} ID found for {} ID: {}".format(util.pretty_ids[to_id], util.pretty_ids[from_id], input_id)) else: raise Failed("AniDB Error: {} ID: {} not found".format(util.pretty_ids[from_id], input_id)) @@ -77,7 +80,7 @@ class AniDBAPI: movie_ids = [] for anidb_id in anime_ids: try: - tmdb_id, tvdb_id = self.convert_from_imdb(self.convert_anidb_to_imdb(anidb_id), language) + tmdb_id = self.convert_from_imdb(self.convert_anidb_to_imdb(anidb_id), language) if tmdb_id: movie_ids.append(tmdb_id) else: raise Failed except Failed: @@ -90,27 +93,34 @@ class AniDBAPI: return movie_ids, show_ids def convert_from_imdb(self, imdb_id, language): - if self.Cache: - tmdb_id, tvdb_id = self.Cache.get_ids_from_imdb(imdb_id) - expired = False - if not tmdb_id: - tmdb_id, expired = self.Cache.get_tmdb_from_imdb(imdb_id) - if expired: - tmdb_id = None - else: - tmdb_id = None - from_cache = tmdb_id is not None + output_tmdb_ids = [] + if not isinstance(imdb_id, list): + imdb_id = [imdb_id] + + for imdb in imdb_id: + if self.Cache: + tmdb_id, tvdb_id = self.Cache.get_ids_from_imdb(imdb) + expired = False + if not tmdb_id: + tmdb_id, expired = self.Cache.get_tmdb_from_imdb(imdb) + if expired: + tmdb_id = None + else: + tmdb_id = None + from_cache = tmdb_id is not None - if not tmdb_id and self.TMDb: - try: tmdb_id = self.TMDb.convert_imdb_to_tmdb(imdb_id) - except Failed: pass - if not tmdb_id and self.Trakt: - try: tmdb_id = self.Trakt.convert_imdb_to_tmdb(imdb_id) - except Failed: pass - try: - if tmdb_id and not from_cache: self.TMDb.get_movie(tmdb_id) - except Failed: tmdb_id = None - if not tmdb_id: raise Failed("TVDb Error: No TMDb ID found for IMDb: {}".format(imdb_id)) - if self.Cache and tmdb_id and expired is not False: - self.Cache.update_imdb("movie", expired, imdb_id, tmdb_id) - return tmdb_id + if not tmdb_id and self.TMDb: + try: tmdb_id = self.TMDb.convert_imdb_to_tmdb(imdb) + except Failed: pass + if not tmdb_id and self.Trakt: + try: tmdb_id = self.Trakt.convert_imdb_to_tmdb(imdb) + except Failed: pass + try: + if tmdb_id and not from_cache: self.TMDb.get_movie(tmdb_id) + except Failed: tmdb_id = None + if tmdb_id: output_tmdb_ids.append(tmdb_id) + if self.Cache and tmdb_id and expired is not False: + self.Cache.update_imdb("movie", expired, imdb, tmdb_id) + if len(output_tmdb_ids) == 0: raise Failed("AniDB Error: No TMDb ID found for IMDb: {}".format(imdb_id)) + elif len(output_tmdb_ids) == 1: return output_tmdb_ids[0] + else: return output_tmdb_ids diff --git a/modules/config.py b/modules/config.py index 6b6d03db..dee019ba 100644 --- a/modules/config.py +++ b/modules/config.py @@ -129,7 +129,7 @@ class Config: self.MyAnimeList = MyAnimeListAPI(self.mal, self.MyAnimeListIDList, authorization) except Failed as e: logger.error(e) - logger.info("My Anime List Connection {}".format("Failed" if self.Trakt is None else "Successful")) + logger.info("My Anime List Connection {}".format("Failed" if self.MyAnimeList is None else "Successful")) else: logger.warning("mal attribute not found") @@ -142,7 +142,8 @@ class Config: self.general["plex"]["token"] = check_for_attribute(self.data, "token", parent="plex", default_is_none=True) if "plex" in self.data else None self.general["plex"]["asset_directory"] = check_for_attribute(self.data, "asset_directory", parent="plex", var_type="path", default=os.path.join(default_dir, "assets")) if "plex" in self.data else os.path.join(default_dir, "assets") self.general["plex"]["sync_mode"] = check_for_attribute(self.data, "sync_mode", parent="plex", default="append", test_list=["append", "sync"], options="| \tappend (Only Add Items to the Collection)\n| \tsync (Add & Remove Items from the Collection)") if "plex" in self.data else "append" - self.general["plex"]["show_unmanaged_collections"] = check_for_attribute(self.data, "show_unmanaged_collections", parent="plex", var_type="bool", default=True) if "plex" in self.data else True + self.general["plex"]["show_unmanaged"] = check_for_attribute(self.data, "show_unmanaged", parent="plex", var_type="bool", default=True) if "plex" in self.data else True + self.general["plex"]["show_filtered"] = check_for_attribute(self.data, "show_filtered", parent="plex", var_type="bool", default=False) if "plex" in self.data else False self.general["radarr"] = {} self.general["radarr"]["url"] = check_for_attribute(self.data, "url", parent="radarr", default_is_none=True) if "radarr" in self.data else None @@ -240,15 +241,25 @@ class Config: else: logger.warning("Config Warning: sync_mode attribute is blank using general value: {}".format(self.general["plex"]["sync_mode"])) - params["show_unmanaged_collections"] = self.general["plex"]["show_unmanaged_collections"] - if "plex" in libs[lib] and "show_unmanaged_collections" in libs[lib]["plex"]: - if libs[lib]["plex"]["show_unmanaged_collections"]: - if isinstance(libs[lib]["plex"]["show_unmanaged_collections"], bool): - params["plex"]["show_unmanaged_collections"] = libs[lib]["plex"]["show_unmanaged_collections"] + params["show_unmanaged"] = self.general["plex"]["show_unmanaged"] + if "plex" in libs[lib] and "show_unmanaged" in libs[lib]["plex"]: + if libs[lib]["plex"]["show_unmanaged"]: + if isinstance(libs[lib]["plex"]["show_unmanaged"], bool): + params["plex"]["show_unmanaged"] = libs[lib]["plex"]["show_unmanaged"] else: - logger.warning("Config Warning: plex sub-attribute show_unmanaged_collections must be either true or false using general value: {}".format(self.general["plex"]["show_unmanaged_collections"])) + logger.warning("Config Warning: plex sub-attribute show_unmanaged must be either true or false using general value: {}".format(self.general["plex"]["show_unmanaged"])) else: - logger.warning("Config Warning: radarr sub-attribute add is blank using general value: {}".format(self.general["radarr"]["add"])) + logger.warning("Config Warning: plex sub-attribute show_unmanaged is blank using general value: {}".format(self.general["plex"]["show_unmanaged"])) + + params["show_filtered"] = self.general["plex"]["show_filtered"] + if "plex" in libs[lib] and "show_filtered" in libs[lib]["plex"]: + if libs[lib]["plex"]["show_filtered"]: + if isinstance(libs[lib]["plex"]["show_filtered"], bool): + params["plex"]["show_filtered"] = libs[lib]["plex"]["show_filtered"] + else: + logger.warning("Config Warning: plex sub-attribute show_filtered must be either true or false using general value: {}".format(self.general["plex"]["show_filtered"])) + else: + logger.warning("Config Warning: plex sub-attribute show_filtered is blank using general value: {}".format(self.general["plex"]["show_filtered"])) params["tmdb"] = self.TMDb params["tvdb"] = self.TVDb @@ -428,6 +439,7 @@ class Config: backgrounds_found = [] collectionless = "plex_collectionless" in collections[c] skip_collection = True + show_filtered = library.show_filtered if "schedule" not in collections[c]: skip_collection = False @@ -581,6 +593,9 @@ class Config: elif method_name == "add_to_arr": if isinstance(collections[c][m], bool): details[method_name] = collections[c][m] else: raise Failed("Collection Error: add_to_arr must be either true or false") + elif method_name == "show_filtered": + if isinstance(collections[c][m], bool): show_filtered = collections[c][m] + else: raise Failed("Collection Error: show_filtered must be either true or false using the default false") elif method_name in util.all_details: details[method_name] = collections[c][m] elif method_name in ["year", "year.not"]: methods.append(("plex_search", [[(method_name, util.get_year_list(collections[c][m], method_name))]])) elif method_name in ["decade", "decade.not"]: methods.append(("plex_search", [[(method_name, util.get_int_list(collections[c][m], util.remove_not(method_name)))]])) @@ -640,10 +655,12 @@ class Config: final_filter = filter if final_filter in util.movie_only_filters and library.is_show: logger.error("Collection Error: {} filter only works for movie libraries".format(final_filter)) + elif collections[c][m][filter] is None: + logger.error("Collection Error: {} filter is blank".format(final_filter)) elif final_filter in util.all_filters: filters.append((final_filter, collections[c][m][filter])) else: - logger.error("Collection Error: {} filter not supported".format(filter)) + logger.error("Collection Error: {} filter not supported".format(final_filter)) elif method_name == "plex_collectionless": new_dictionary = {} @@ -782,13 +799,13 @@ class Config: elif current_time.month in [10, 11, 12]: new_dictionary["season"] = "fall" new_dictionary["year"] = get_int(method_name, "year", collections[c][m], current_time.year, min=1917, max=current_time.year + 1) new_dictionary["limit"] = get_int(method_name, "limit", collections[c][m], 100, max=500) - if "sort_by" not in collections[c][m]: logger.warning("Collection Error: mal_season sort_by attribute not found using members as default") - elif not collections[c][m]["sort_by"]: logger.warning("Collection Error: mal_season sort_by attribute is blank using members as default") - elif collections[c][m]["sort_by"] not in util.mal_season_sort: logger.warning("Collection Error: mal_season sort_by attribute {} invalid must be either 'members' or 'score' using members as default".format(collections[c][m]["sort_by"])) + if "sort_by" not in collections[c][m]: logger.warning("Collection Warning: mal_season sort_by attribute not found using members as default") + elif not collections[c][m]["sort_by"]: logger.warning("Collection Warning: mal_season sort_by attribute is blank using members as default") + elif collections[c][m]["sort_by"] not in util.mal_season_sort: logger.warning("Collection Warning: mal_season sort_by attribute {} invalid must be either 'members' or 'score' using members as default".format(collections[c][m]["sort_by"])) else: new_dictionary["sort_by"] = util.mal_season_sort[collections[c][m]["sort_by"]] - if "season" not in collections[c][m]: logger.warning("Collection Error: mal_season season attribute not found using the current season: {} as default".format(new_dictionary["season"])) - elif not collections[c][m]["season"]: logger.warning("Collection Error: mal_season season attribute is blank using the current season: {} as default".format(new_dictionary["season"])) - elif collections[c][m]["season"] not in util.pretty_seasons: logger.warning("Collection Error: mal_season season attribute {} invalid must be either 'winter', 'spring', 'summer' or 'fall' using the current season: {} as default".format(collections[c][m]["season"], new_dictionary["season"])) + if "season" not in collections[c][m]: logger.warning("Collection Warning: mal_season season attribute not found using the current season: {} as default".format(new_dictionary["season"])) + elif not collections[c][m]["season"]: logger.warning("Collection Warning: mal_season season attribute is blank using the current season: {} as default".format(new_dictionary["season"])) + elif collections[c][m]["season"] not in util.pretty_seasons: logger.warning("Collection Warning: mal_season season attribute {} invalid must be either 'winter', 'spring', 'summer' or 'fall' using the current season: {} as default".format(collections[c][m]["season"], new_dictionary["season"])) else: new_dictionary["season"] = collections[c][m]["season"] methods.append((method_name, [new_dictionary])) elif method_name == "mal_userlist": @@ -796,13 +813,13 @@ class Config: if "username" not in collections[c][m]: raise Failed("Collection Error: mal_userlist username attribute is required") elif not collections[c][m]["username"]: raise Failed("Collection Error: mal_userlist username attribute is blank") else: new_dictionary["username"] = collections[c][m]["username"] - if "status" not in collections[c][m]: logger.warning("Collection Error: mal_season status attribute not found using all as default") - elif not collections[c][m]["status"]: logger.warning("Collection Error: mal_season status attribute is blank using all as default") - elif collections[c][m]["status"] not in util.mal_userlist_status: logger.warning("Collection Error: mal_season status attribute {} invalid must be either 'all', 'watching', 'completed', 'on_hold', 'dropped' or 'plan_to_watch' using all as default".format(collections[c][m]["status"])) + if "status" not in collections[c][m]: logger.warning("Collection Warning: mal_season status attribute not found using all as default") + elif not collections[c][m]["status"]: logger.warning("Collection Warning: mal_season status attribute is blank using all as default") + elif collections[c][m]["status"] not in util.mal_userlist_status: logger.warning("Collection Warning: mal_season status attribute {} invalid must be either 'all', 'watching', 'completed', 'on_hold', 'dropped' or 'plan_to_watch' using all as default".format(collections[c][m]["status"])) else: new_dictionary["status"] = util.mal_userlist_status[collections[c][m]["status"]] - if "sort_by" not in collections[c][m]: logger.warning("Collection Error: mal_season sort_by attribute not found using score as default") - elif not collections[c][m]["sort_by"]: logger.warning("Collection Error: mal_season sort_by attribute is blank using score as default") - elif collections[c][m]["sort_by"] not in util.mal_userlist_sort: logger.warning("Collection Error: mal_season sort_by attribute {} invalid must be either 'score', 'last_updated', 'title' or 'start_date' using score as default".format(collections[c][m]["sort_by"])) + if "sort_by" not in collections[c][m]: logger.warning("Collection Warning: mal_season sort_by attribute not found using score as default") + elif not collections[c][m]["sort_by"]: logger.warning("Collection Warning: mal_season sort_by attribute is blank using score as default") + elif collections[c][m]["sort_by"] not in util.mal_userlist_sort: logger.warning("Collection Warning: mal_season sort_by attribute {} invalid must be either 'score', 'last_updated', 'title' or 'start_date' using score as default".format(collections[c][m]["sort_by"])) else: new_dictionary["sort_by"] = util.mal_userlist_sort[collections[c][m]["sort_by"]] new_dictionary["limit"] = get_int(method_name, "limit", collections[c][m], 100, max=1000) methods.append((method_name, [new_dictionary])) @@ -940,24 +957,36 @@ class Config: elif "trakt" in method: items_found += check_map(self.Trakt.get_items(method, value, library.is_movie)) else: logger.error("Collection Error: {} method not supported".format(method)) - if len(items) > 0: map = library.add_to_collection(collection_obj if collection_obj else collection_name, items, filters, map=map) + if len(items) > 0: map = library.add_to_collection(collection_obj if collection_obj else collection_name, items, filters, show_filtered, map, movie_map, show_map) else: logger.error("No items found to add to this collection ") if len(missing_movies) > 0 or len(missing_shows) > 0: logger.info("") if len(missing_movies) > 0: + not_lang = None + terms = None + for filter_method, filter_data in filters: + if filter_method.startswith("original_language"): + terms = filter_data if isinstance(filter_data, list) else [lang.strip().lower() for lang in str(filter_data).split(",")] + not_lang = filter_method.endswith(".not") + break + missing_movies_with_names = [] for missing_id in missing_movies: try: - title = str(self.TMDb.get_movie(missing_id).title) - missing_movies_with_names.append((title, missing_id)) - logger.info("{} Collection | ? | {} (TMDb: {})".format(collection_name, title, missing_id)) + movie = self.TMDb.get_movie(missing_id) + title = str(movie.title) + if not_lang is None or (not_lang is True and movie.original_language not in terms) or (not_lang is False and movie.original_language in terms): + missing_movies_with_names.append((title, missing_id)) + logger.info("{} Collection | ? | {} (TMDb: {})".format(collection_name, title, missing_id)) + elif show_filtered is True: + logger.info("{} Collection | X | {} (TMDb: {})".format(collection_name, title, missing_id)) except Failed as e: logger.error(e) - logger.info("{} Movie{} Missing".format(len(missing_movies), "s" if len(missing_movies) > 1 else "")) + logger.info("{} Movie{} Missing".format(len(missing_movies_with_names), "s" if len(missing_movies_with_names) > 1 else "")) library.save_missing(collection_name, missing_movies_with_names, True) if do_arr and library.Radarr: - library.Radarr.add_tmdb(missing_movies) + library.Radarr.add_tmdb([missing_id for title, missing_id in missing_movies_with_names]) if len(missing_shows) > 0 and library.is_show: missing_shows_with_names = [] for missing_id in missing_shows: @@ -1040,10 +1069,36 @@ class Config: if background[0] == "url": plex_collection.uploadArt(url=background[1]) else: plex_collection.uploadArt(filepath=background[1]) logger.info("Detail: {} updated background to [{}] {}".format(background[2], background[0], background[1])) + + if library.asset_directory: + path = os.path.join(library.asset_directory, "{}".format(name_mapping)) + dirs = [folder for folder in os.listdir(path) if os.path.isdir(os.path.join(path, folder))] + if len(dirs) > 0: + for item in plex_collection.items(): + folder = os.path.basename(os.path.dirname(item.locations[0])) + if folder in dirs: + files = [file for file in os.listdir(os.path.join(path, folder)) if os.path.isfile(os.path.join(path, folder, file))] + poster_path = None + background_path = None + for file in files: + if poster_path is None and file.startswith("poster."): + poster_path = os.path.join(path, folder, file) + if background_path is None and file.startswith("background."): + background_path = os.path.join(path, folder, file) + if poster_path: + item.uploadPoster(filepath=poster_path) + logger.info("Detail: asset_directory updated {}'s poster to [file] {}".format(item.title, poster_path)) + if background_path: + item.uploadArt(filepath=background_path) + logger.info("Detail: asset_directory updated {}'s background to [file] {}".format(item.title, background_path)) + if poster_path is None and background_path is None: + logger.warning("No Files Found: {}".format(os.path.join(path, folder))) + else: + logger.warning("No Folder: {}".format(os.path.join(path, folder))) except Exception as e: util.print_stacktrace() logger.error("Unknown Error: {}".format(e)) - if library.show_unmanaged_collections is True: + if library.show_unmanaged is True: logger.info("") util.seperator("Unmanaged Collections in {} Library".format(library.name)) logger.info("") @@ -1067,10 +1122,14 @@ class Config: for i, item in enumerate(items, 1): length = util.print_return(length, "Processing: {}/{} {}".format(i, len(items), item.title)) id_type, main_id = self.get_id(item, library, length) - if id_type == "movie": - movie_map[main_id] = item.ratingKey - elif id_type == "show": - show_map[main_id] = item.ratingKey + if isinstance(main_id, list): + if id_type == "movie": + for m in main_id: movie_map[m] = item.ratingKey + elif id_type == "show": + for m in main_id: show_map[m] = item.ratingKey + else: + if id_type == "movie": movie_map[main_id] = item.ratingKey + elif id_type == "show": show_map[main_id] = item.ratingKey util.print_end(length, "Processed {} {}".format(len(items), "Movies" if library.is_movie else "Shows")) return movie_map, show_map @@ -1130,6 +1189,17 @@ class Config: if mal_id and not tmdb_id: try: tmdb_id = self.MyAnimeListIDList.convert_mal_to_tmdb(mal_id) except Failed: pass + if not tmdb_id and imdb_id and isinstance(imdb_id, list) and self.TMDb: + tmdb_id = [] + new_imdb_id = [] + for imdb in imdb_id: + try: + temp_tmdb_id = self.TMDb.convert_imdb_to_tmdb(imdb) + tmdb_id.append(temp_tmdb_id) + new_imdb_id.append(imdb) + except Failed: + continue + imdb_id = new_imdb_id if not tmdb_id and imdb_id and self.TMDb: try: tmdb_id = self.TMDb.convert_imdb_to_tmdb(imdb_id) except Failed: pass @@ -1160,18 +1230,6 @@ class Config: if not tvdb_id and imdb_id and self.Trakt and library.is_show: try: tvdb_id = self.Trakt.convert_imdb_to_tvdb(imdb_id) except Failed: pass - if tvdb_id and not anidb_id: - try: anidb_id = self.AniDB.convert_tvdb_to_anidb(tvdb_id) - except Failed: pass - if imdb_id and not anidb_id: - try: anidb_id = self.AniDB.convert_imdb_to_anidb(imdb_id) - except Failed: pass - if tvdb_id and not mal_id: - try: mal_id = self.MyAnimeListIDList.convert_tvdb_to_mal(tvdb_id) - except Failed: pass - if tmdb_id and not mal_id: - try: mal_id = self.MyAnimeListIDList.convert_tmdb_to_mal(tmdb_id) - except Failed: pass if (not tmdb_id and library.is_movie) or (not tvdb_id and not ((anidb_id or mal_id) and tmdb_id) and library.is_show): service_name = "TMDb ID" if library.is_movie else "TVDb ID" @@ -1194,8 +1252,13 @@ class Config: elif id_name: error_message = "Configure TMDb or Trakt to covert {} to {}".format(id_name, service_name) else: error_message = "No ID to convert to {}".format(service_name) if self.Cache and (tmdb_id and library.is_movie) or ((tvdb_id or ((anidb_id or mal_id) and tmdb_id)) and library.is_show): - util.print_end(length, "Cache | {} | {:<46} | {:<6} | {:<10} | {:<6} | {:<5} | {:<5} | {}".format("^" if expired is True else "+", item.guid, tmdb_id if tmdb_id else "None", imdb_id if imdb_id else "None", tvdb_id if tvdb_id else "None", anidb_id if anidb_id else "None", mal_id if mal_id else "None", item.title)) - self.Cache.update_guid("movie" if library.is_movie else "show", item.guid, tmdb_id, imdb_id, tvdb_id, anidb_id, mal_id, expired) + if isinstance(tmdb_id, list): + for i in range(len(tmdb_id)): + util.print_end(length, "Cache | {} | {:<46} | {:<6} | {:<10} | {:<6} | {:<5} | {:<5} | {}".format("^" if expired is True else "+", item.guid, tmdb_id[i] if tmdb_id[i] else "None", imdb_id[i] if imdb_id[i] else "None", tvdb_id if tvdb_id else "None", anidb_id if anidb_id else "None", mal_id if mal_id else "None", item.title)) + self.Cache.update_guid("movie" if library.is_movie else "show", item.guid, tmdb_id[i], imdb_id[i], tvdb_id, anidb_id, mal_id, expired) + else: + util.print_end(length, "Cache | {} | {:<46} | {:<6} | {:<10} | {:<6} | {:<5} | {:<5} | {}".format("^" if expired is True else "+", item.guid, tmdb_id if tmdb_id else "None", imdb_id if imdb_id else "None", tvdb_id if tvdb_id else "None", anidb_id if anidb_id else "None", mal_id if mal_id else "None", item.title)) + self.Cache.update_guid("movie" if library.is_movie else "show", item.guid, tmdb_id, imdb_id, tvdb_id, anidb_id, mal_id, expired) if tmdb_id and library.is_movie: return "movie", tmdb_id elif tvdb_id and library.is_show: return "show", tvdb_id elif (anidb_id or mal_id) and tmdb_id: return "movie", tmdb_id diff --git a/modules/plex.py b/modules/plex.py index d366bc8e..99ce3c8f 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -68,13 +68,16 @@ class PlexAPI: except Failed as e: logger.error(e) logger.info("{} library's Tautulli Connection {}".format(params["name"], "Failed" if self.Tautulli is None else "Successful")) + self.TMDb = params["tmdb"] + self.TVDb = params["tvdb"] self.name = params["name"] self.missing_path = os.path.join(os.path.dirname(os.path.abspath(params["metadata_path"])), "{}_missing.yml".format(os.path.splitext(os.path.basename(params["metadata_path"]))[0])) self.metadata_path = params["metadata_path"] self.asset_directory = params["asset_directory"] self.sync_mode = params["sync_mode"] - self.show_unmanaged_collections = params["show_unmanaged_collections"] + self.show_unmanaged = params["show_unmanaged"] + self.show_filtered = params["show_filtered"] self.plex = params["plex"] self.radarr = params["radarr"] self.sonarr = params["sonarr"] @@ -160,7 +163,7 @@ class PlexAPI: except yaml.scanner.ScannerError as e: logger.error("YAML Error: {}".format(str(e).replace("\n", "\n|\t "))) - def add_to_collection(self, collection, items, filters, map={}): + def add_to_collection(self, collection, items, filters, show_filtered, map, movie_map, show_map): name = collection.title if isinstance(collection, Collections) else collection collection_items = collection.items() if isinstance(collection, Collections) else [] total = len(items) @@ -181,6 +184,23 @@ class PlexAPI: if attr is None or attr < threshold_date: match = False break + elif method == "original_language": + terms = f[1] if isinstance(f[1], list) else [lang.lower() for lang in str(f[1]).split(", ")] + tmdb_id = None + movie = None + for key, value in movie_map.items(): + if current.ratingKey == value: + try: + movie = self.TMDb.get_movie(key) + break + except Failed: + pass + if movie is None: + logger.warning("Filter Error: No TMDb ID found for {}".format(current.title)) + continue + if (modifier == ".not" and movie.original_language in terms) or (modifier != ".not" and movie.original_language not in terms): + match = False + break elif modifier in [".gte", ".lte"]: if method == "originallyAvailableAt": threshold_date = datetime.strptime(f[1], "%m/%d/%y") @@ -212,6 +232,8 @@ class PlexAPI: util.print_end(length, "{} Collection | {} | {}".format(name, "=" if current in collection_items else "+", current.title)) if current in collection_items: map[current.ratingKey] = None else: current.addCollection(name) + elif show_filtered is True: + logger.info("{} Collection | X | {}".format(name, current.title)) media_type = "{}{}".format("Movie" if self.is_movie else "Show", "s" if total > 1 else "") util.print_end(length, "{} {} Processed".format(total, media_type)) return map @@ -294,10 +316,8 @@ class PlexAPI: add_edit("originally_available", str(item.originallyAvailableAt)[:-9], self.metadata[m], key="originallyAvailableAt", value=originally_available) add_edit("rating", item.rating, self.metadata[m], value=rating) add_edit("content_rating", item.contentRating, self.metadata[m], key="contentRating") - if self.is_movie: - add_edit("original_title", item.originalTitle, self.metadata[m], key="originalTitle", value=original_title) - elif "original_title" in self.metadata[m]: - logger.error("Metadata Error: original_title does not work with shows") + originalTitle = item.originalTitle if self.is_movie else item._data.attrib.get("originalTitle") + add_edit("original_title", originalTitle, self.metadata[m], key="originalTitle", value=original_title) add_edit("studio", item.studio, self.metadata[m], value=studio) add_edit("tagline", item.tagline, self.metadata[m], value=tagline) add_edit("summary", item.summary, self.metadata[m], value=summary) diff --git a/modules/tvdb.py b/modules/tvdb.py index 73c7a381..f483305e 100644 --- a/modules/tvdb.py +++ b/modules/tvdb.py @@ -121,11 +121,11 @@ class TVDbAPI: if status_message: logger.info("Processing {}: {}".format(pretty, data)) if method == "tvdb_show": - try: show_ids.append(self.get_series(language, tvdb_id=int(data))) - except ValueError: show_ids.append(self.get_series(language, tvdb_url=data)) + try: show_ids.append(self.get_series(language, tvdb_id=int(data)).id) + except ValueError: show_ids.append(self.get_series(language, tvdb_url=data).id) elif method == "tvdb_movie": - try: movie_ids.append(self.get_movie(language, tvdb_id=int(data))) - except ValueError: movie_ids.append(self.get_movie(language, tvdb_url=data)) + try: movie_ids.append(self.get_movie(language, tvdb_id=int(data)).id) + except ValueError: movie_ids.append(self.get_movie(language, tvdb_url=data).id) elif method == "tvdb_list": tmdb_ids, tvdb_ids = self.get_tvdb_ids_from_url(data, language) movie_ids.extend(tmdb_ids) diff --git a/modules/util.py b/modules/util.py index 91104620..b707b4c9 100644 --- a/modules/util.py +++ b/modules/util.py @@ -43,6 +43,7 @@ filter_alias = { "genre": "genres", "max_age": "max_age", "originally_available": "originallyAvailableAt", + "original_language": "original_language", "rating": "rating", "studio": "studio", "subtitle_language": "subtitle_language", @@ -361,6 +362,7 @@ all_filters = [ "genre", "genre.not", "max_age", "originally_available.gte", "originally_available.lte", + "original_language", "original_language.not", "rating.gte", "rating.lte", "studio", "studio.not", "subtitle_language", "subtitle_language.not", @@ -372,6 +374,7 @@ movie_only_filters = [ "audio_language", "audio_language.not", "country", "country.not", "director", "director.not", + "original_language", "original_language.not", "subtitle_language", "subtitle_language.not", "video_resolution", "video_resolution.not", "writer", "writer.not" diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 8bfff9eb..4e19b595 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -56,7 +56,7 @@ logger.info(util.get_centered_text("| |_) | |/ _ \ \/ / | |\/| |/ _ \ __/ _` | | logger.info(util.get_centered_text("| __/| | __/> < | | | | __/ || (_| | | | | | (_| | | | | (_| | (_| | __/ | ")) logger.info(util.get_centered_text("|_| |_|\___/_/\_\ |_| |_|\___|\__\__,_| |_| |_|\__,_|_| |_|\__,_|\__, |\___|_| ")) logger.info(util.get_centered_text(" |___/ ")) -logger.info(util.get_centered_text(" Version: 1.0.3 ")) +logger.info(util.get_centered_text(" Version: 1.1.0 ")) util.seperator() if args.test: