Merge pull request #30 from meisnate12/develop

v1.1.0
This commit is contained in:
meisnate12 2021-02-14 02:55:45 -05:00 committed by GitHub
commit f25a13982d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 184 additions and 87 deletions

View file

@ -1,5 +1,5 @@
# Plex Meta Manager # 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. 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'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) * 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 * Pull Request are welcome
* [Buy Me a Pizza](https://www.buymeacoffee.com/meisnate12) * [Buy Me a Pizza](https://www.buymeacoffee.com/meisnate12)

View file

@ -13,7 +13,8 @@ plex: # Can be individually specified pe
token: #################### token: ####################
sync_mode: append sync_mode: append
asset_directory: config/assets asset_directory: config/assets
show_unmanaged_collections: true show_unmanaged: true
show_filtered: false
radarr: # Can be individually specified per library as well radarr: # Can be individually specified per library as well
url: http://192.168.1.12:7878 url: http://192.168.1.12:7878
token: ################################ token: ################################

View file

@ -23,9 +23,12 @@ class AniDBAPI:
def convert_tvdb_to_anidb(self, tvdb_id): return self.convert_anidb(tvdb_id, "tvdbid", "anidbid") 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_imdb_to_anidb(self, imdb_id): return self.convert_anidb(imdb_id, "imdbid", "anidbid")
def convert_anidb(self, input_id, from_id, to_id): 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:
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: 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)) else: raise Failed("AniDB Error: {} ID: {} not found".format(util.pretty_ids[from_id], input_id))
@ -77,7 +80,7 @@ class AniDBAPI:
movie_ids = [] movie_ids = []
for anidb_id in anime_ids: for anidb_id in anime_ids:
try: 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) if tmdb_id: movie_ids.append(tmdb_id)
else: raise Failed else: raise Failed
except Failed: except Failed:
@ -90,27 +93,34 @@ class AniDBAPI:
return movie_ids, show_ids return movie_ids, show_ids
def convert_from_imdb(self, imdb_id, language): def convert_from_imdb(self, imdb_id, language):
if self.Cache: output_tmdb_ids = []
tmdb_id, tvdb_id = self.Cache.get_ids_from_imdb(imdb_id) if not isinstance(imdb_id, list):
expired = False imdb_id = [imdb_id]
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
if not tmdb_id and self.TMDb: for imdb in imdb_id:
try: tmdb_id = self.TMDb.convert_imdb_to_tmdb(imdb_id) if self.Cache:
except Failed: pass tmdb_id, tvdb_id = self.Cache.get_ids_from_imdb(imdb)
if not tmdb_id and self.Trakt: expired = False
try: tmdb_id = self.Trakt.convert_imdb_to_tmdb(imdb_id) if not tmdb_id:
except Failed: pass tmdb_id, expired = self.Cache.get_tmdb_from_imdb(imdb)
try: if expired:
if tmdb_id and not from_cache: self.TMDb.get_movie(tmdb_id) tmdb_id = None
except Failed: tmdb_id = None else:
if not tmdb_id: raise Failed("TVDb Error: No TMDb ID found for IMDb: {}".format(imdb_id)) tmdb_id = None
if self.Cache and tmdb_id and expired is not False: from_cache = tmdb_id is not None
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

View file

@ -129,7 +129,7 @@ class Config:
self.MyAnimeList = MyAnimeListAPI(self.mal, self.MyAnimeListIDList, authorization) self.MyAnimeList = MyAnimeListAPI(self.mal, self.MyAnimeListIDList, authorization)
except Failed as e: except Failed as e:
logger.error(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: else:
logger.warning("mal attribute not found") 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"]["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"]["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"]["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"] = {}
self.general["radarr"]["url"] = check_for_attribute(self.data, "url", parent="radarr", default_is_none=True) if "radarr" in self.data else None 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: else:
logger.warning("Config Warning: sync_mode attribute is blank using general value: {}".format(self.general["plex"]["sync_mode"])) 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"] params["show_unmanaged"] = self.general["plex"]["show_unmanaged"]
if "plex" in libs[lib] and "show_unmanaged_collections" in libs[lib]["plex"]: if "plex" in libs[lib] and "show_unmanaged" in libs[lib]["plex"]:
if libs[lib]["plex"]["show_unmanaged_collections"]: if libs[lib]["plex"]["show_unmanaged"]:
if isinstance(libs[lib]["plex"]["show_unmanaged_collections"], bool): if isinstance(libs[lib]["plex"]["show_unmanaged"], bool):
params["plex"]["show_unmanaged_collections"] = libs[lib]["plex"]["show_unmanaged_collections"] params["plex"]["show_unmanaged"] = libs[lib]["plex"]["show_unmanaged"]
else: 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: 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["tmdb"] = self.TMDb
params["tvdb"] = self.TVDb params["tvdb"] = self.TVDb
@ -428,6 +439,7 @@ class Config:
backgrounds_found = [] backgrounds_found = []
collectionless = "plex_collectionless" in collections[c] collectionless = "plex_collectionless" in collections[c]
skip_collection = True skip_collection = True
show_filtered = library.show_filtered
if "schedule" not in collections[c]: if "schedule" not in collections[c]:
skip_collection = False skip_collection = False
@ -581,6 +593,9 @@ class Config:
elif method_name == "add_to_arr": elif method_name == "add_to_arr":
if isinstance(collections[c][m], bool): details[method_name] = collections[c][m] 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") 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 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 ["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)))]])) 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 final_filter = filter
if final_filter in util.movie_only_filters and library.is_show: 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)) 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: elif final_filter in util.all_filters:
filters.append((final_filter, collections[c][m][filter])) filters.append((final_filter, collections[c][m][filter]))
else: 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": elif method_name == "plex_collectionless":
new_dictionary = {} new_dictionary = {}
@ -782,13 +799,13 @@ class Config:
elif current_time.month in [10, 11, 12]: new_dictionary["season"] = "fall" 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["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) 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") 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 Error: mal_season sort_by attribute is blank 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 Error: mal_season sort_by attribute {} invalid must be either 'members' or 'score' using members as default".format(collections[c][m]["sort_by"])) 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"]] 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"])) 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 Error: mal_season season attribute is blank 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 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"])) 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"] else: new_dictionary["season"] = collections[c][m]["season"]
methods.append((method_name, [new_dictionary])) methods.append((method_name, [new_dictionary]))
elif method_name == "mal_userlist": 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") 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") elif not collections[c][m]["username"]: raise Failed("Collection Error: mal_userlist username attribute is blank")
else: new_dictionary["username"] = collections[c][m]["username"] 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") 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 Error: mal_season status attribute is blank 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 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"])) 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"]] 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") 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 Error: mal_season sort_by attribute is blank 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 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"])) 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"]] 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) new_dictionary["limit"] = get_int(method_name, "limit", collections[c][m], 100, max=1000)
methods.append((method_name, [new_dictionary])) 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)) 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)) 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 ") else: logger.error("No items found to add to this collection ")
if len(missing_movies) > 0 or len(missing_shows) > 0: if len(missing_movies) > 0 or len(missing_shows) > 0:
logger.info("") logger.info("")
if len(missing_movies) > 0: 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 = [] missing_movies_with_names = []
for missing_id in missing_movies: for missing_id in missing_movies:
try: try:
title = str(self.TMDb.get_movie(missing_id).title) movie = self.TMDb.get_movie(missing_id)
missing_movies_with_names.append((title, missing_id)) title = str(movie.title)
logger.info("{} Collection | ? | {} (TMDb: {})".format(collection_name, title, missing_id)) 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: except Failed as e:
logger.error(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) library.save_missing(collection_name, missing_movies_with_names, True)
if do_arr and library.Radarr: 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: if len(missing_shows) > 0 and library.is_show:
missing_shows_with_names = [] missing_shows_with_names = []
for missing_id in missing_shows: for missing_id in missing_shows:
@ -1040,10 +1069,36 @@ class Config:
if background[0] == "url": plex_collection.uploadArt(url=background[1]) if background[0] == "url": plex_collection.uploadArt(url=background[1])
else: plex_collection.uploadArt(filepath=background[1]) else: plex_collection.uploadArt(filepath=background[1])
logger.info("Detail: {} updated background to [{}] {}".format(background[2], background[0], 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: except Exception as e:
util.print_stacktrace() util.print_stacktrace()
logger.error("Unknown Error: {}".format(e)) logger.error("Unknown Error: {}".format(e))
if library.show_unmanaged_collections is True: if library.show_unmanaged is True:
logger.info("") logger.info("")
util.seperator("Unmanaged Collections in {} Library".format(library.name)) util.seperator("Unmanaged Collections in {} Library".format(library.name))
logger.info("") logger.info("")
@ -1067,10 +1122,14 @@ class Config:
for i, item in enumerate(items, 1): for i, item in enumerate(items, 1):
length = util.print_return(length, "Processing: {}/{} {}".format(i, len(items), item.title)) length = util.print_return(length, "Processing: {}/{} {}".format(i, len(items), item.title))
id_type, main_id = self.get_id(item, library, length) id_type, main_id = self.get_id(item, library, length)
if id_type == "movie": if isinstance(main_id, list):
movie_map[main_id] = item.ratingKey if id_type == "movie":
elif id_type == "show": for m in main_id: movie_map[m] = item.ratingKey
show_map[main_id] = 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")) util.print_end(length, "Processed {} {}".format(len(items), "Movies" if library.is_movie else "Shows"))
return movie_map, show_map return movie_map, show_map
@ -1130,6 +1189,17 @@ class Config:
if mal_id and not tmdb_id: if mal_id and not tmdb_id:
try: tmdb_id = self.MyAnimeListIDList.convert_mal_to_tmdb(mal_id) try: tmdb_id = self.MyAnimeListIDList.convert_mal_to_tmdb(mal_id)
except Failed: pass 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: if not tmdb_id and imdb_id and self.TMDb:
try: tmdb_id = self.TMDb.convert_imdb_to_tmdb(imdb_id) try: tmdb_id = self.TMDb.convert_imdb_to_tmdb(imdb_id)
except Failed: pass except Failed: pass
@ -1160,18 +1230,6 @@ class Config:
if not tvdb_id and imdb_id and self.Trakt and library.is_show: 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) try: tvdb_id = self.Trakt.convert_imdb_to_tvdb(imdb_id)
except Failed: pass 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): 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" 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) 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) 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): 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)) if isinstance(tmdb_id, list):
self.Cache.update_guid("movie" if library.is_movie else "show", item.guid, tmdb_id, imdb_id, tvdb_id, anidb_id, mal_id, expired) 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 if tmdb_id and library.is_movie: return "movie", tmdb_id
elif tvdb_id and library.is_show: return "show", tvdb_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 elif (anidb_id or mal_id) and tmdb_id: return "movie", tmdb_id

View file

@ -68,13 +68,16 @@ class PlexAPI:
except Failed as e: logger.error(e) except Failed as e: logger.error(e)
logger.info("{} library's Tautulli Connection {}".format(params["name"], "Failed" if self.Tautulli is None else "Successful")) 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.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.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.metadata_path = params["metadata_path"]
self.asset_directory = params["asset_directory"] self.asset_directory = params["asset_directory"]
self.sync_mode = params["sync_mode"] 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.plex = params["plex"]
self.radarr = params["radarr"] self.radarr = params["radarr"]
self.sonarr = params["sonarr"] self.sonarr = params["sonarr"]
@ -160,7 +163,7 @@ class PlexAPI:
except yaml.scanner.ScannerError as e: except yaml.scanner.ScannerError as e:
logger.error("YAML Error: {}".format(str(e).replace("\n", "\n|\t "))) 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 name = collection.title if isinstance(collection, Collections) else collection
collection_items = collection.items() if isinstance(collection, Collections) else [] collection_items = collection.items() if isinstance(collection, Collections) else []
total = len(items) total = len(items)
@ -181,6 +184,23 @@ class PlexAPI:
if attr is None or attr < threshold_date: if attr is None or attr < threshold_date:
match = False match = False
break 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"]: elif modifier in [".gte", ".lte"]:
if method == "originallyAvailableAt": if method == "originallyAvailableAt":
threshold_date = datetime.strptime(f[1], "%m/%d/%y") 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)) util.print_end(length, "{} Collection | {} | {}".format(name, "=" if current in collection_items else "+", current.title))
if current in collection_items: map[current.ratingKey] = None if current in collection_items: map[current.ratingKey] = None
else: current.addCollection(name) 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 "") media_type = "{}{}".format("Movie" if self.is_movie else "Show", "s" if total > 1 else "")
util.print_end(length, "{} {} Processed".format(total, media_type)) util.print_end(length, "{} {} Processed".format(total, media_type))
return map 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("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("rating", item.rating, self.metadata[m], value=rating)
add_edit("content_rating", item.contentRating, self.metadata[m], key="contentRating") add_edit("content_rating", item.contentRating, self.metadata[m], key="contentRating")
if self.is_movie: originalTitle = item.originalTitle if self.is_movie else item._data.attrib.get("originalTitle")
add_edit("original_title", item.originalTitle, self.metadata[m], key="originalTitle", value=original_title) add_edit("original_title", 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")
add_edit("studio", item.studio, self.metadata[m], value=studio) add_edit("studio", item.studio, self.metadata[m], value=studio)
add_edit("tagline", item.tagline, self.metadata[m], value=tagline) add_edit("tagline", item.tagline, self.metadata[m], value=tagline)
add_edit("summary", item.summary, self.metadata[m], value=summary) add_edit("summary", item.summary, self.metadata[m], value=summary)

View file

@ -121,11 +121,11 @@ class TVDbAPI:
if status_message: if status_message:
logger.info("Processing {}: {}".format(pretty, data)) logger.info("Processing {}: {}".format(pretty, data))
if method == "tvdb_show": if method == "tvdb_show":
try: show_ids.append(self.get_series(language, tvdb_id=int(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)) except ValueError: show_ids.append(self.get_series(language, tvdb_url=data).id)
elif method == "tvdb_movie": elif method == "tvdb_movie":
try: movie_ids.append(self.get_movie(language, tvdb_id=int(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)) except ValueError: movie_ids.append(self.get_movie(language, tvdb_url=data).id)
elif method == "tvdb_list": elif method == "tvdb_list":
tmdb_ids, tvdb_ids = self.get_tvdb_ids_from_url(data, language) tmdb_ids, tvdb_ids = self.get_tvdb_ids_from_url(data, language)
movie_ids.extend(tmdb_ids) movie_ids.extend(tmdb_ids)

View file

@ -43,6 +43,7 @@ filter_alias = {
"genre": "genres", "genre": "genres",
"max_age": "max_age", "max_age": "max_age",
"originally_available": "originallyAvailableAt", "originally_available": "originallyAvailableAt",
"original_language": "original_language",
"rating": "rating", "rating": "rating",
"studio": "studio", "studio": "studio",
"subtitle_language": "subtitle_language", "subtitle_language": "subtitle_language",
@ -361,6 +362,7 @@ all_filters = [
"genre", "genre.not", "genre", "genre.not",
"max_age", "max_age",
"originally_available.gte", "originally_available.lte", "originally_available.gte", "originally_available.lte",
"original_language", "original_language.not",
"rating.gte", "rating.lte", "rating.gte", "rating.lte",
"studio", "studio.not", "studio", "studio.not",
"subtitle_language", "subtitle_language.not", "subtitle_language", "subtitle_language.not",
@ -372,6 +374,7 @@ movie_only_filters = [
"audio_language", "audio_language.not", "audio_language", "audio_language.not",
"country", "country.not", "country", "country.not",
"director", "director.not", "director", "director.not",
"original_language", "original_language.not",
"subtitle_language", "subtitle_language.not", "subtitle_language", "subtitle_language.not",
"video_resolution", "video_resolution.not", "video_resolution", "video_resolution.not",
"writer", "writer.not" "writer", "writer.not"

View file

@ -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("|_| |_|\___/_/\_\ |_| |_|\___|\__\__,_| |_| |_|\__,_|_| |_|\__,_|\__, |\___|_| "))
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() util.seperator()
if args.test: if args.test: