Merge pull request #83 from meisnate12/develop

v1.4.0
pull/144/head v1.4.0
meisnate12 4 years ago committed by GitHub
commit 706b2c33c8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,11 +1,13 @@
# Plex Meta Manager # Plex Meta Manager
#### Version 1.3.0 #### Version 1.4.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.
The script can update many metadata fields for movies, shows, collections, seasons, and episodes and can act as a backup if your plex DB goes down. It can even update metadata the plex UI can't like Season Names. If the time is put into the metadata configuration file you can have a way to recreate your library and all its metadata changes with the click of a button. The script can update many metadata fields for movies, shows, collections, seasons, and episodes and can act as a backup if your plex DB goes down. It can even update metadata the plex UI can't like Season Names. If the time is put into the metadata configuration file you can have a way to recreate your library and all its metadata changes with the click of a button.
The script is designed to work with most Metadata agents including the new Plex Movie Agent, [Hama Anime Agent](https://github.com/ZeroQI/Hama.bundle), and [MyAnimeList Anime Agent](https://github.com/Fribb/MyAnimeList.bundle). The script is designed to work with most Metadata agents including the new Plex Movie Agent, New Plex TV Agent, [Hama Anime Agent](https://github.com/ZeroQI/Hama.bundle), and [MyAnimeList Anime Agent](https://github.com/Fribb/MyAnimeList.bundle).
[![paypal](https://www.paypalobjects.com/en_US/i/btn/btn_donateCC_LG.gif)](https://www.paypal.com/donate?business=JTK3CVKF3ZHP2&item_name=Plex+Meta+Manager&currency_code=USD)
## Getting Started ## Getting Started
@ -20,4 +22,3 @@ The script is designed to work with most Metadata agents including the new Plex
* 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) * 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 but please submit them to the develop branch * Pull Request are welcome but please submit them to the develop branch
* If you wish to contribute to the Wiki please fork and send a pull request on the [Plex Meta Manager Wiki Repository](https://github.com/meisnate12/Plex-Meta-Manager-Wiki) * If you wish to contribute to the Wiki please fork and send a pull request on the [Plex Meta Manager Wiki Repository](https://github.com/meisnate12/Plex-Meta-Manager-Wiki)
* [Buy Me a Pizza](https://www.buymeacoffee.com/meisnate12)

@ -43,6 +43,8 @@ sonarr: # Can be individually specified
root_folder_path: "S:/TV Shows" root_folder_path: "S:/TV Shows"
add: false add: false
search: false search: false
omdb:
apikey: ########
trakt: trakt:
client_id: ################################################################ client_id: ################################################################
client_secret: ################################################################ client_secret: ################################################################

@ -7,10 +7,8 @@ from retrying import retry
logger = logging.getLogger("Plex Meta Manager") logger = logging.getLogger("Plex Meta Manager")
class AniDBAPI: class AniDBAPI:
def __init__(self, Cache=None, TMDb=None, Trakt=None): def __init__(self, config):
self.Cache = Cache self.config = config
self.TMDb = TMDb
self.Trakt = Trakt
self.urls = { self.urls = {
"anime": "https://anidb.net/anime", "anime": "https://anidb.net/anime",
"popular": "https://anidb.net/latest/anime/popular/?h=1", "popular": "https://anidb.net/latest/anime/popular/?h=1",
@ -80,7 +78,8 @@ class AniDBAPI:
movie_ids = [] movie_ids = []
for anidb_id in anime_ids: for anidb_id in anime_ids:
try: try:
tmdb_id = self.convert_from_imdb(self.convert_anidb_to_imdb(anidb_id)) for imdb_id in self.convert_anidb_to_imdb(anidb_id):
tmdb_id, _ = self.config.convert_from_imdb(imdb_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:
@ -91,36 +90,3 @@ class AniDBAPI:
logger.debug(f"TMDb IDs Found: {movie_ids}") logger.debug(f"TMDb IDs Found: {movie_ids}")
logger.debug(f"TVDb IDs Found: {show_ids}") logger.debug(f"TVDb IDs Found: {show_ids}")
return movie_ids, show_ids return movie_ids, show_ids
def convert_from_imdb(self, imdb_id):
output_tmdb_ids = []
if not isinstance(imdb_id, list):
imdb_id = [imdb_id]
for imdb in imdb_id:
expired = False
if self.Cache:
tmdb_id, tvdb_id = self.Cache.get_ids_from_imdb(imdb)
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)
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(f"AniDB Error: No TMDb ID found for IMDb: {imdb_id}")
elif len(output_tmdb_ids) == 1: return output_tmdb_ids[0]
else: return output_tmdb_ids

@ -240,6 +240,14 @@ class CollectionBuilder:
self.summaries[method_name] = config.TMDb.get_list(util.regex_first_int(data[m], "TMDb List ID")).description self.summaries[method_name] = config.TMDb.get_list(util.regex_first_int(data[m], "TMDb List ID")).description
elif method_name == "tmdb_biography": elif method_name == "tmdb_biography":
self.summaries[method_name] = config.TMDb.get_person(util.regex_first_int(data[m], "TMDb Person ID")).biography self.summaries[method_name] = config.TMDb.get_person(util.regex_first_int(data[m], "TMDb Person ID")).biography
elif method_name == "tvdb_summary":
self.summaries[method_name] = config.TVDb.get_movie_or_show(data[m], self.library.Plex.language, self.library.is_movie).summary
elif method_name == "tvdb_description":
self.summaries[method_name] = config.TVDb.get_list_description(data[m], self.library.Plex.language)
elif method_name == "trakt_description":
self.summaries[method_name] = config.Trakt.standard_list(config.Trakt.validate_trakt_list(util.get_list(data[m]))[0]).description
elif method_name == "letterboxd_description":
self.summaries[method_name] = config.Letterboxd.get_list_description(data[m], self.library.Plex.language)
elif method_name == "collection_mode": elif method_name == "collection_mode":
if data[m] in ["default", "hide", "hide_items", "show_items", "hideItems", "showItems"]: if data[m] in ["default", "hide", "hide_items", "show_items", "hideItems", "showItems"]:
if data[m] == "hide_items": self.details[method_name] = "hideItems" if data[m] == "hide_items": self.details[method_name] = "hideItems"
@ -258,6 +266,8 @@ class CollectionBuilder:
self.posters[method_name] = f"{config.TMDb.image_url}{config.TMDb.get_movie_show_or_collection(util.regex_first_int(data[m], 'TMDb ID'), self.library.is_movie).poster_path}" self.posters[method_name] = f"{config.TMDb.image_url}{config.TMDb.get_movie_show_or_collection(util.regex_first_int(data[m], 'TMDb ID'), self.library.is_movie).poster_path}"
elif method_name == "tmdb_profile": elif method_name == "tmdb_profile":
self.posters[method_name] = f"{config.TMDb.image_url}{config.TMDb.get_person(util.regex_first_int(data[m], 'TMDb Person ID')).profile_path}" self.posters[method_name] = f"{config.TMDb.image_url}{config.TMDb.get_person(util.regex_first_int(data[m], 'TMDb Person ID')).profile_path}"
elif method_name == "tvdb_poster":
self.posters[method_name] = f"{config.TVDb.get_movie_or_series(data[m], self.library.Plex.language, self.library.is_movie).poster_path}"
elif method_name == "file_poster": elif method_name == "file_poster":
if os.path.exists(data[m]): self.posters[method_name] = os.path.abspath(data[m]) if os.path.exists(data[m]): self.posters[method_name] = os.path.abspath(data[m])
else: raise Failed(f"Collection Error: Poster Path Does Not Exist: {os.path.abspath(data[m])}") else: raise Failed(f"Collection Error: Poster Path Does Not Exist: {os.path.abspath(data[m])}")
@ -265,6 +275,8 @@ class CollectionBuilder:
self.backgrounds[method_name] = data[m] self.backgrounds[method_name] = data[m]
elif method_name == "tmdb_background": elif method_name == "tmdb_background":
self.backgrounds[method_name] = f"{config.TMDb.image_url}{config.TMDb.get_movie_show_or_collection(util.regex_first_int(data[m], 'TMDb ID'), self.library.is_movie).poster_path}" self.backgrounds[method_name] = f"{config.TMDb.image_url}{config.TMDb.get_movie_show_or_collection(util.regex_first_int(data[m], 'TMDb ID'), self.library.is_movie).poster_path}"
elif method_name == "tvdb_background":
self.posters[method_name] = f"{config.TVDb.get_movie_or_series(data[m], self.library.Plex.language, self.library.is_movie).background_path}"
elif method_name == "file_background": elif method_name == "file_background":
if os.path.exists(data[m]): self.backgrounds[method_name] = os.path.abspath(data[m]) if os.path.exists(data[m]): self.backgrounds[method_name] = os.path.abspath(data[m])
else: raise Failed(f"Collection Error: Background Path Does Not Exist: {os.path.abspath(data[m])}") else: raise Failed(f"Collection Error: Background Path Does Not Exist: {os.path.abspath(data[m])}")
@ -294,6 +306,8 @@ class CollectionBuilder:
else: else:
final_values.append(value) final_values.append(value)
self.methods.append(("plex_search", [[(method_name, final_values)]])) self.methods.append(("plex_search", [[(method_name, final_values)]]))
elif method_name == "title":
self.methods.append(("plex_search", [[(method_name, data[m])]]))
elif method_name in util.plex_searches: elif method_name in util.plex_searches:
self.methods.append(("plex_search", [[(method_name, util.get_list(data[m]))]])) self.methods.append(("plex_search", [[(method_name, util.get_list(data[m]))]]))
elif method_name == "plex_all": elif method_name == "plex_all":
@ -313,6 +327,12 @@ class CollectionBuilder:
self.methods.append((method_name, config.AniDB.validate_anidb_list(util.get_int_list(data[m], "AniDB ID"), self.library.Plex.language))) self.methods.append((method_name, config.AniDB.validate_anidb_list(util.get_int_list(data[m], "AniDB ID"), self.library.Plex.language)))
elif method_name == "trakt_list": elif method_name == "trakt_list":
self.methods.append((method_name, config.Trakt.validate_trakt_list(util.get_list(data[m])))) self.methods.append((method_name, config.Trakt.validate_trakt_list(util.get_list(data[m]))))
elif method_name == "trakt_list_details":
valid_list = config.Trakt.validate_trakt_list(util.get_list(data[m]))
item = config.Trakt.standard_list(valid_list[0])
if hasattr(item, "description") and item.description:
self.summaries[method_name] = item.description
self.methods.append((method_name[:-8], valid_list))
elif method_name == "trakt_watchlist": elif method_name == "trakt_watchlist":
self.methods.append((method_name, config.Trakt.validate_trakt_watchlist(util.get_list(data[m]), self.library.is_movie))) self.methods.append((method_name, config.Trakt.validate_trakt_watchlist(util.get_list(data[m]), self.library.is_movie)))
elif method_name == "imdb_list": elif method_name == "imdb_list":
@ -327,6 +347,12 @@ class CollectionBuilder:
list_count = 0 list_count = 0
new_list.append({"url": imdb_url, "limit": list_count}) new_list.append({"url": imdb_url, "limit": list_count})
self.methods.append((method_name, new_list)) self.methods.append((method_name, new_list))
elif method_name == "letterboxd_list":
self.methods.append((method_name, util.get_list(data[m], split=False)))
elif method_name == "letterboxd_list_details":
values = util.get_list(data[m], split=False)
self.summaries[method_name] = config.Letterboxd.get_list_description(values[0], self.library.Plex.language)
self.methods.append((method_name[:-8], values))
elif method_name in util.dictionary_lists: elif method_name in util.dictionary_lists:
if isinstance(data[m], dict): if isinstance(data[m], dict):
def get_int(parent, method, data_in, default_in, minimum=1, maximum=None): def get_int(parent, method, data_in, default_in, minimum=1, maximum=None):
@ -402,6 +428,9 @@ class CollectionBuilder:
if len(years) > 0: if len(years) > 0:
used.append(util.remove_not(search)) used.append(util.remove_not(search))
searches.append((search, util.get_int_list(data[m][s], util.remove_not(search)))) searches.append((search, util.get_int_list(data[m][s], util.remove_not(search))))
elif search == "title":
used.append(util.remove_not(search))
searches.append((search, data[m][s]))
elif search in util.plex_searches: elif search in util.plex_searches:
used.append(util.remove_not(search)) used.append(util.remove_not(search))
searches.append((search, util.get_list(data[m][s]))) searches.append((search, util.get_list(data[m][s])))
@ -521,6 +550,30 @@ class CollectionBuilder:
logger.warning(f"Collection Warning: {method_name} must be an integer greater then 0 defaulting to 20") logger.warning(f"Collection Warning: {method_name} must be an integer greater then 0 defaulting to 20")
list_count = 20 list_count = 20
self.methods.append((method_name, [list_count])) self.methods.append((method_name, [list_count]))
elif "tvdb" in method_name:
values = util.get_list(data[m])
if method_name[-8:] == "_details":
if method_name == "tvdb_movie_details":
item = config.TVDb.get_movie(self.library.Plex.language, values[0])
if hasattr(item, "description") and item.description:
self.summaries[method_name] = item.description
if hasattr(item, "background_path") and item.background_path:
self.backgrounds[method_name] = f"{config.TMDb.image_url}{item.background_path}"
if hasattr(item, "poster_path") and item.poster_path:
self.posters[method_name] = f"{config.TMDb.image_url}{item.poster_path}"
elif method_name == "tvdb_show_details":
item = config.TVDb.get_series(self.library.Plex.language, values[0])
if hasattr(item, "description") and item.description:
self.summaries[method_name] = item.description
if hasattr(item, "background_path") and item.background_path:
self.backgrounds[method_name] = f"{config.TMDb.image_url}{item.background_path}"
if hasattr(item, "poster_path") and item.poster_path:
self.posters[method_name] = f"{config.TMDb.image_url}{item.poster_path}"
elif method_name == "tvdb_list_details":
self.summaries[method_name] = config.TVDb.get_list_description(values[0], self.library.Plex.language)
self.methods.append((method_name[:-8], values))
else:
self.methods.append((method_name, values))
elif method_name in util.tmdb_lists: elif method_name in util.tmdb_lists:
values = config.TMDb.validate_tmdb_list(util.get_int_list(data[m], f"TMDb {util.tmdb_type[method_name]} ID"), util.tmdb_type[method_name]) values = config.TMDb.validate_tmdb_list(util.get_int_list(data[m], f"TMDb {util.tmdb_type[method_name]} ID"), util.tmdb_type[method_name])
if method_name[-8:] == "_details": if method_name[-8:] == "_details":
@ -549,8 +602,10 @@ class CollectionBuilder:
self.methods.append((method_name, util.get_list(data[m]))) self.methods.append((method_name, util.get_list(data[m])))
elif method_name not in util.other_attributes: elif method_name not in util.other_attributes:
raise Failed(f"Collection Error: {method_name} attribute not supported") raise Failed(f"Collection Error: {method_name} attribute not supported")
else: elif m in util.all_lists or m in util.method_alias or m in util.plex_searches:
raise Failed(f"Collection Error: {m} attribute is blank") raise Failed(f"Collection Error: {m} attribute is blank")
else:
logger.warning(f"Collection Warning: {m} attribute is blank")
self.sync = self.library.sync_mode == "sync" self.sync = self.library.sync_mode == "sync"
if "sync_mode" in data: if "sync_mode" in data:
@ -600,17 +655,32 @@ class CollectionBuilder:
items_found += len(items) items_found += len(items)
elif method == "plex_search": elif method == "plex_search":
search_terms = {} search_terms = {}
for i, attr_pair in enumerate(value): title_search = None
search_list = attr_pair[1] has_processed = False
final_method = attr_pair[0][:-4] + "!" if attr_pair[0][-4:] == ".not" else attr_pair[0] for search_method, search_data in value:
if search_method == "title":
title_search = search_data
logger.info(f"Processing {pretty}: title({title_search})")
has_processed = True
for search_method, search_list in value:
if search_method != "title":
final_method = search_method[:-4] + "!" if search_method[-4:] == ".not" else search_method
if self.library.is_show: if self.library.is_show:
final_method = "show." + final_method final_method = "show." + final_method
search_terms[final_method] = search_list search_terms[final_method] = search_list
ors = "" ors = ""
for o, param in enumerate(attr_pair[1]): for o, param in enumerate(search_list):
or_des = " OR " if o > 0 else f"{attr_pair[0]}(" or_des = " OR " if o > 0 else f"{search_method}("
ors += f"{or_des}{param}" ors += f"{or_des}{param}"
logger.info(f"\t\t AND {ors})" if i > 0 else f"Processing {pretty}: {ors})") if title_search or has_processed:
logger.info(f"\t\t AND {ors})")
else:
logger.info(f"Processing {pretty}: {ors})")
has_processed = True
if title_search:
items = self.library.Plex.search(title_search, **search_terms)
else:
items = self.library.Plex.search(**search_terms) items = self.library.Plex.search(**search_terms)
items_found += len(items) items_found += len(items)
elif method == "plex_collectionless": elif method == "plex_collectionless":
@ -648,6 +718,7 @@ class CollectionBuilder:
elif "mal" in method: items_found += check_map(self.config.MyAnimeList.get_items(method, value)) elif "mal" in method: items_found += check_map(self.config.MyAnimeList.get_items(method, value))
elif "tvdb" in method: items_found += check_map(self.config.TVDb.get_items(method, value, self.library.Plex.language)) elif "tvdb" in method: items_found += check_map(self.config.TVDb.get_items(method, value, self.library.Plex.language))
elif "imdb" in method: items_found += check_map(self.config.IMDb.get_items(method, value, self.library.Plex.language)) elif "imdb" in method: items_found += check_map(self.config.IMDb.get_items(method, value, self.library.Plex.language))
elif "letterboxd" in method: items_found += check_map(self.config.Letterboxd.get_items(method, value, self.library.Plex.language))
elif "tmdb" in method: items_found += check_map(self.config.TMDb.get_items(method, value, self.library.is_movie)) elif "tmdb" in method: items_found += check_map(self.config.TMDb.get_items(method, value, self.library.is_movie))
elif "trakt" in method: items_found += check_map(self.config.Trakt.get_items(method, value, self.library.is_movie)) elif "trakt" in method: items_found += check_map(self.config.Trakt.get_items(method, value, self.library.is_movie))
else: logger.error(f"Collection Error: {method} method not supported") else: logger.error(f"Collection Error: {method} method not supported")
@ -694,7 +765,7 @@ class CollectionBuilder:
missing_shows_with_names = [] missing_shows_with_names = []
for missing_id in missing_shows: for missing_id in missing_shows:
try: try:
title = str(self.config.TVDb.get_series(self.library.Plex.language, tvdb_id=missing_id).title.encode("ascii", "replace").decode()) title = str(self.config.TVDb.get_series(self.library.Plex.language, missing_id).title.encode("ascii", "replace").decode())
except Failed as e: except Failed as e:
logger.error(e) logger.error(e)
continue continue
@ -738,10 +809,13 @@ class CollectionBuilder:
return summaries[summary_method] return summaries[summary_method]
if "summary" in self.summaries: summary = get_summary("summary", self.summaries) if "summary" in self.summaries: summary = get_summary("summary", self.summaries)
elif "tmdb_description" in self.summaries: summary = get_summary("tmdb_description", self.summaries) elif "tmdb_description" in self.summaries: summary = get_summary("tmdb_description", self.summaries)
elif "letterboxd_description" in self.summaries: summary = get_summary("letterboxd_description", self.summaries)
elif "tmdb_summary" in self.summaries: summary = get_summary("tmdb_summary", self.summaries) elif "tmdb_summary" in self.summaries: summary = get_summary("tmdb_summary", self.summaries)
elif "tvdb_summary" in self.summaries: summary = get_summary("tvdb_summary", self.summaries)
elif "tmdb_biography" in self.summaries: summary = get_summary("tmdb_biography", self.summaries) elif "tmdb_biography" in self.summaries: summary = get_summary("tmdb_biography", self.summaries)
elif "tmdb_person" in self.summaries: summary = get_summary("tmdb_person", self.summaries) elif "tmdb_person" in self.summaries: summary = get_summary("tmdb_person", self.summaries)
elif "tmdb_collection_details" in self.summaries: summary = get_summary("tmdb_collection_details", self.summaries) elif "tmdb_collection_details" in self.summaries: summary = get_summary("tmdb_collection_details", self.summaries)
elif "trakt_list_details" in self.summaries: summary = get_summary("trakt_list_details", self.summaries)
elif "tmdb_list_details" in self.summaries: summary = get_summary("tmdb_list_details", self.summaries) elif "tmdb_list_details" in self.summaries: summary = get_summary("tmdb_list_details", self.summaries)
elif "tmdb_actor_details" in self.summaries: summary = get_summary("tmdb_actor_details", self.summaries) elif "tmdb_actor_details" in self.summaries: summary = get_summary("tmdb_actor_details", self.summaries)
elif "tmdb_crew_details" in self.summaries: summary = get_summary("tmdb_crew_details", self.summaries) elif "tmdb_crew_details" in self.summaries: summary = get_summary("tmdb_crew_details", self.summaries)
@ -749,6 +823,8 @@ class CollectionBuilder:
elif "tmdb_producer_details" in self.summaries: summary = get_summary("tmdb_producer_details", self.summaries) elif "tmdb_producer_details" in self.summaries: summary = get_summary("tmdb_producer_details", self.summaries)
elif "tmdb_writer_details" in self.summaries: summary = get_summary("tmdb_writer_details", self.summaries) elif "tmdb_writer_details" in self.summaries: summary = get_summary("tmdb_writer_details", self.summaries)
elif "tmdb_movie_details" in self.summaries: summary = get_summary("tmdb_movie_details", self.summaries) elif "tmdb_movie_details" in self.summaries: summary = get_summary("tmdb_movie_details", self.summaries)
elif "tvdb_movie_details" in self.summaries: summary = get_summary("tvdb_movie_details", self.summaries)
elif "tvdb_show_details" in self.summaries: summary = get_summary("tvdb_show_details", self.summaries)
elif "tmdb_show_details" in self.summaries: summary = get_summary("tmdb_show_details", self.summaries) elif "tmdb_show_details" in self.summaries: summary = get_summary("tmdb_show_details", self.summaries)
else: summary = None else: summary = None
if summary: if summary:
@ -810,7 +886,7 @@ class CollectionBuilder:
dirs = [folder for folder in os.listdir(path) if os.path.isdir(os.path.join(path, folder))] dirs = [folder for folder in os.listdir(path) if os.path.isdir(os.path.join(path, folder))]
if len(dirs) > 0: if len(dirs) > 0:
for item in collection.items(): for item in collection.items():
folder = os.path.basename(os.path.dirname(item.locations[0])) folder = os.path.basename(os.path.dirname(item.locations[0]) if self.library.is_movie else item.locations[0])
if folder in dirs: if folder in dirs:
matches = glob.glob(os.path.join(path, folder, "poster.*")) matches = glob.glob(os.path.join(path, folder, "poster.*"))
poster_path = os.path.abspath(matches[0]) if len(matches) > 0 else None poster_path = os.path.abspath(matches[0]) if len(matches) > 0 else None
@ -824,6 +900,13 @@ class CollectionBuilder:
logger.info(f"Detail: asset_directory updated {item.title}'s background to [file] {background_path}") logger.info(f"Detail: asset_directory updated {item.title}'s background to [file] {background_path}")
if poster_path is None and background_path is None: if poster_path is None and background_path is None:
logger.warning(f"No Files Found: {os.path.join(path, folder)}") logger.warning(f"No Files Found: {os.path.join(path, folder)}")
if self.library.is_show:
for season in item.seasons():
matches = glob.glob(os.path.join(path, folder, f"Season{'0' if season.seasonNumber < 10 else ''}{season.seasonNumber}.*"))
if len(matches) > 0:
season_path = os.path.abspath(matches[0])
season.uploadPoster(filepath=season_path)
logger.info(f"Detail: asset_directory updated {item.title} Season {season.seasonNumber}'s poster to [file] {season_path}")
else: else:
logger.warning(f"No Folder: {os.path.join(path, folder)}") logger.warning(f"No Folder: {os.path.join(path, folder)}")
@ -847,16 +930,19 @@ class CollectionBuilder:
elif "file_poster" in self.posters: set_image("file_poster", self.posters) elif "file_poster" in self.posters: set_image("file_poster", self.posters)
elif "tmdb_poster" in self.posters: set_image("tmdb_poster", self.posters) elif "tmdb_poster" in self.posters: set_image("tmdb_poster", self.posters)
elif "tmdb_profile" in self.posters: set_image("tmdb_profile", self.posters) elif "tmdb_profile" in self.posters: set_image("tmdb_profile", self.posters)
elif "tvdb_poster" in self.posters: set_image("tvdb_poster", self.posters)
elif "asset_directory" in self.posters: set_image("asset_directory", self.posters) elif "asset_directory" in self.posters: set_image("asset_directory", self.posters)
elif "tmdb_person" in self.posters: set_image("tmdb_person", self.posters) elif "tmdb_person" in self.posters: set_image("tmdb_person", self.posters)
elif "tmdb_collection_details" in self.posters: set_image("tmdb_collection", self.posters) elif "tmdb_collection_details" in self.posters: set_image("tmdb_collection_details", self.posters)
elif "tmdb_actor_details" in self.posters: set_image("tmdb_actor_details", self.posters) elif "tmdb_actor_details" in self.posters: set_image("tmdb_actor_details", self.posters)
elif "tmdb_crew_details" in self.posters: set_image("tmdb_crew_details", self.posters) elif "tmdb_crew_details" in self.posters: set_image("tmdb_crew_details", self.posters)
elif "tmdb_director_details" in self.posters: set_image("tmdb_director_details", self.posters) elif "tmdb_director_details" in self.posters: set_image("tmdb_director_details", self.posters)
elif "tmdb_producer_details" in self.posters: set_image("tmdb_producer_details", self.posters) elif "tmdb_producer_details" in self.posters: set_image("tmdb_producer_details", self.posters)
elif "tmdb_writer_details" in self.posters: set_image("tmdb_writer_details", self.posters) elif "tmdb_writer_details" in self.posters: set_image("tmdb_writer_details", self.posters)
elif "tmdb_movie_details" in self.posters: set_image("tmdb_movie", self.posters) elif "tmdb_movie_details" in self.posters: set_image("tmdb_movie_details", self.posters)
elif "tmdb_show_details" in self.posters: set_image("tmdb_show", self.posters) elif "tvdb_movie_details" in self.posters: set_image("tvdb_movie_details", self.posters)
elif "tvdb_show_details" in self.posters: set_image("tvdb_show_details", self.posters)
elif "tmdb_show_details" in self.posters: set_image("tmdb_show_details", self.posters)
else: logger.info("No poster to update") else: logger.info("No poster to update")
logger.info("") logger.info("")
@ -867,25 +953,28 @@ class CollectionBuilder:
logger.info(f"Method: {b} Background: {self.backgrounds[b]}") logger.info(f"Method: {b} Background: {self.backgrounds[b]}")
if "url_background" in self.backgrounds: set_image("url_background", self.backgrounds, is_background=True) if "url_background" in self.backgrounds: set_image("url_background", self.backgrounds, is_background=True)
elif "file_background" in self.backgrounds: set_image("file_poster", self.backgrounds, is_background=True) elif "file_background" in self.backgrounds: set_image("file_background", self.backgrounds, is_background=True)
elif "tmdb_background" in self.backgrounds: set_image("tmdb_poster", self.backgrounds, is_background=True) elif "tmdb_background" in self.backgrounds: set_image("tmdb_background", self.backgrounds, is_background=True)
elif "tvdb_background" in self.backgrounds: set_image("tvdb_background", self.backgrounds, is_background=True)
elif "asset_directory" in self.backgrounds: set_image("asset_directory", self.backgrounds, is_background=True) elif "asset_directory" in self.backgrounds: set_image("asset_directory", self.backgrounds, is_background=True)
elif "tmdb_collection_details" in self.backgrounds: set_image("tmdb_collection", self.backgrounds, is_background=True) elif "tmdb_collection_details" in self.backgrounds: set_image("tmdb_collection_details", self.backgrounds, is_background=True)
elif "tmdb_movie_details" in self.backgrounds: set_image("tmdb_movie", self.backgrounds, is_background=True) elif "tmdb_movie_details" in self.backgrounds: set_image("tmdb_movie_details", self.backgrounds, is_background=True)
elif "tmdb_show_details" in self.backgrounds: set_image("tmdb_show", self.backgrounds, is_background=True) elif "tvdb_movie_details" in self.backgrounds: set_image("tvdb_movie_details", self.backgrounds, is_background=True)
elif "tvdb_show_details" in self.backgrounds: set_image("tvdb_show_details", self.backgrounds, is_background=True)
elif "tmdb_show_details" in self.backgrounds: set_image("tmdb_show_details", self.backgrounds, is_background=True)
else: logger.info("No background to update") else: logger.info("No background to update")
def run_collections_again(self, library, collection_obj, movie_map, show_map): def run_collections_again(self, collection_obj, movie_map, show_map):
collection_items = collection_obj.items() if isinstance(collection_obj, Collections) else [] collection_items = collection_obj.items() if isinstance(collection_obj, Collections) else []
name = collection_obj.title if isinstance(collection_obj, Collections) else collection_obj name = collection_obj.title if isinstance(collection_obj, Collections) else collection_obj
rating_keys = [movie_map[mm] for mm in self.missing_movies if mm in movie_map] rating_keys = [movie_map[mm] for mm in self.missing_movies if mm in movie_map]
if library.is_show: if self.library.is_show:
rating_keys.extend([show_map[sm] for sm in self.missing_shows if sm in show_map]) rating_keys.extend([show_map[sm] for sm in self.missing_shows if sm in show_map])
if len(rating_keys) > 0: if len(rating_keys) > 0:
for rating_key in rating_keys: for rating_key in rating_keys:
try: try:
current = library.fetchItem(int(rating_key)) current = self.library.fetchItem(int(rating_key))
except (BadRequest, NotFound): except (BadRequest, NotFound):
logger.error(f"Plex Error: Item {rating_key} not found") logger.error(f"Plex Error: Item {rating_key} not found")
continue continue
@ -894,7 +983,7 @@ class CollectionBuilder:
else: else:
current.addCollection(name) current.addCollection(name)
logger.info(f"{name} Collection | + | {current.title}") logger.info(f"{name} Collection | + | {current.title}")
logger.info(f"{len(rating_keys)} {'Movie' if library.is_movie else 'Show'}{'s' if len(rating_keys) > 1 else ''} Processed") logger.info(f"{len(rating_keys)} {'Movie' if self.library.is_movie else 'Show'}{'s' if len(rating_keys) > 1 else ''} Processed")
if len(self.missing_movies) > 0: if len(self.missing_movies) > 0:
logger.info("") logger.info("")
@ -910,12 +999,12 @@ class CollectionBuilder:
logger.info("") logger.info("")
logger.info(f"{len(self.missing_movies)} Movie{'s' if len(self.missing_movies) > 1 else ''} Missing") logger.info(f"{len(self.missing_movies)} Movie{'s' if len(self.missing_movies) > 1 else ''} Missing")
if len(self.missing_shows) > 0 and library.is_show: if len(self.missing_shows) > 0 and self.library.is_show:
logger.info("") logger.info("")
for missing_id in self.missing_shows: for missing_id in self.missing_shows:
if missing_id not in show_map: if missing_id not in show_map:
try: try:
title = str(self.config.TVDb.get_series(self.library.Plex.language, tvdb_id=missing_id).title.encode("ascii", "replace").decode()) title = str(self.config.TVDb.get_series(self.library.Plex.language, missing_id).title.encode("ascii", "replace").decode())
except Failed as e: except Failed as e:
logger.error(e) logger.error(e)
continue continue

@ -1,6 +1,7 @@
import logging, os, random, sqlite3 import logging, os, random, sqlite3
from contextlib import closing from contextlib import closing
from datetime import datetime, timedelta from datetime import datetime, timedelta
from modules.util import Failed
logger = logging.getLogger("Plex Meta Manager") logger = logging.getLogger("Plex Meta Manager")
@ -13,10 +14,12 @@ class Cache:
cursor.execute("SELECT count(name) FROM sqlite_master WHERE type='table' AND name='guids'") cursor.execute("SELECT count(name) FROM sqlite_master WHERE type='table' AND name='guids'")
if cursor.fetchone()[0] == 0: if cursor.fetchone()[0] == 0:
logger.info(f"Initializing cache database at {cache}") logger.info(f"Initializing cache database at {cache}")
else:
logger.info(f"Using cache database at {cache}")
cursor.execute( cursor.execute(
"""CREATE TABLE IF NOT EXISTS guids ( """CREATE TABLE IF NOT EXISTS guids (
INTEGER PRIMARY KEY, INTEGER PRIMARY KEY,
plex_guid TEXT, plex_guid TEXT UNIQUE,
tmdb_id TEXT, tmdb_id TEXT,
imdb_id TEXT, imdb_id TEXT,
tvdb_id TEXT, tvdb_id TEXT,
@ -28,13 +31,25 @@ class Cache:
cursor.execute( cursor.execute(
"""CREATE TABLE IF NOT EXISTS imdb_map ( """CREATE TABLE IF NOT EXISTS imdb_map (
INTEGER PRIMARY KEY, INTEGER PRIMARY KEY,
imdb_id TEXT, imdb_id TEXT UNIQUE,
t_id TEXT, t_id TEXT,
expiration_date TEXT, expiration_date TEXT,
media_type TEXT)""" media_type TEXT)"""
) )
else: cursor.execute(
logger.info(f"Using cache database at {cache}") """CREATE TABLE IF NOT EXISTS omdb_data (
INTEGER PRIMARY KEY,
imdb_id TEXT UNIQUE,
title TEXT,
year INTEGER,
content_rating TEXT,
genres TEXT,
imdb_rating REAL,
imdb_votes INTEGER,
metacritic_rating INTEGER,
type TEXT,
expiration_date TEXT)"""
)
self.expiration = expiration self.expiration = expiration
self.cache_path = cache self.cache_path = cache
@ -82,6 +97,40 @@ class Cache:
expired = time_between_insertion.days > self.expiration expired = time_between_insertion.days > self.expiration
return id_to_return, expired return id_to_return, expired
def get_ids(self, media_type, plex_guid=None, tmdb_id=None, imdb_id=None, tvdb_id=None):
ids_to_return = {}
expired = None
if plex_guid:
key = plex_guid
key_type = "plex_guid"
elif tmdb_id:
key = tmdb_id
key_type = "tmdb_id"
elif imdb_id:
key = imdb_id
key_type = "imdb_id"
elif tvdb_id:
key = tvdb_id
key_type = "tvdb_id"
else:
raise Failed("ID Required")
with sqlite3.connect(self.cache_path) as connection:
connection.row_factory = sqlite3.Row
with closing(connection.cursor()) as cursor:
cursor.execute(f"SELECT * FROM guids WHERE {key_type} = ? AND media_type = ?", (key, media_type))
row = cursor.fetchone()
if row:
if row["plex_guid"]: ids_to_return["plex"] = row["plex_guid"]
if row["tmdb_id"]: ids_to_return["tmdb"] = int(row["tmdb_id"])
if row["imdb_id"]: ids_to_return["imdb"] = row["imdb_id"]
if row["tvdb_id"]: ids_to_return["tvdb"] = int(row["tvdb_id"])
if row["anidb_id"]: ids_to_return["anidb"] = int(row["anidb_id"])
if row["mal_id"]: ids_to_return["mal"] = int(row["mal_id"])
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 ids_to_return, expired
def update_guid(self, media_type, plex_guid, tmdb_id, imdb_id, tvdb_id, anidb_id, mal_id, expired): def update_guid(self, media_type, plex_guid, tmdb_id, imdb_id, tvdb_id, anidb_id, mal_id, expired):
expiration_date = datetime.now() if expired is True else (datetime.now() - timedelta(days=random.randint(1, self.expiration))) 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: with sqlite3.connect(self.cache_path) as connection:
@ -126,3 +175,35 @@ class Cache:
with closing(connection.cursor()) as cursor: with closing(connection.cursor()) as cursor:
cursor.execute("INSERT OR IGNORE INTO imdb_map(imdb_id) VALUES(?)", (imdb_id,)) cursor.execute("INSERT OR IGNORE INTO imdb_map(imdb_id) VALUES(?)", (imdb_id,))
cursor.execute("UPDATE imdb_map SET t_id = ?, expiration_date = ?, media_type = ? WHERE imdb_id = ?", (t_id, expiration_date.strftime("%Y-%m-%d"), media_type, imdb_id)) cursor.execute("UPDATE imdb_map SET t_id = ?, expiration_date = ?, media_type = ? WHERE imdb_id = ?", (t_id, expiration_date.strftime("%Y-%m-%d"), media_type, imdb_id))
def query_omdb(self, imdb_id):
omdb_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 omdb_data WHERE imdb_id = ?", (imdb_id,))
row = cursor.fetchone()
if row:
omdb_dict["imdbID"] = row["imdb_id"] if row["imdb_id"] else None
omdb_dict["Title"] = row["title"] if row["title"] else None
omdb_dict["Year"] = row["year"] if row["year"] else None
omdb_dict["Rated"] = row["content_rating"] if row["content_rating"] else None
omdb_dict["Genre"] = row["genres"] if row["genres"] else None
omdb_dict["imdbRating"] = row["imdb_rating"] if row["imdb_rating"] else None
omdb_dict["imdbVotes"] = row["imdb_votes"] if row["imdb_votes"] else None
omdb_dict["Metascore"] = row["metacritic_rating"] if row["metacritic_rating"] else None
omdb_dict["Type"] = row["type"] if row["type"] 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 omdb_dict, expired
def update_omdb(self, expired, omdb):
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 omdb_data(imdb_id) VALUES(?)", (omdb.imdb_id,))
update_sql = "UPDATE omdb_data SET title = ?, year = ?, content_rating = ?, genres = ?, imdb_rating = ?, imdb_votes = ?, metacritic_rating = ?, type = ?, expiration_date = ? WHERE imdb_id = ?"
cursor.execute(update_sql, (omdb.title, omdb.year, omdb.content_rating, omdb.genres_str, omdb.imdb_rating, omdb.imdb_votes, omdb.metacritic_rating, omdb.type, expiration_date.strftime("%Y-%m-%d"), omdb.imdb_id))

@ -4,8 +4,10 @@ from modules.anidb import AniDBAPI
from modules.builder import CollectionBuilder from modules.builder import CollectionBuilder
from modules.cache import Cache from modules.cache import Cache
from modules.imdb import IMDbAPI from modules.imdb import IMDbAPI
from modules.letterboxd import LetterboxdAPI
from modules.mal import MyAnimeListAPI from modules.mal import MyAnimeListAPI
from modules.mal import MyAnimeListIDList from modules.mal import MyAnimeListIDList
from modules.omdb import OMDbAPI
from modules.plex import PlexAPI from modules.plex import PlexAPI
from modules.radarr import RadarrAPI from modules.radarr import RadarrAPI
from modules.sonarr import SonarrAPI from modules.sonarr import SonarrAPI
@ -15,6 +17,7 @@ from modules.trakttv import TraktAPI
from modules.tvdb import TVDbAPI from modules.tvdb import TVDbAPI
from modules.util import Failed from modules.util import Failed
from plexapi.exceptions import BadRequest from plexapi.exceptions import BadRequest
from plexapi.media import Guid
from ruamel import yaml from ruamel import yaml
logger = logging.getLogger("Plex Meta Manager") logger = logging.getLogger("Plex Meta Manager")
@ -69,6 +72,7 @@ class Config:
if "tautulli" in new_config: new_config["tautulli"] = new_config.pop("tautulli") if "tautulli" in new_config: new_config["tautulli"] = new_config.pop("tautulli")
if "radarr" in new_config: new_config["radarr"] = new_config.pop("radarr") if "radarr" in new_config: new_config["radarr"] = new_config.pop("radarr")
if "sonarr" in new_config: new_config["sonarr"] = new_config.pop("sonarr") if "sonarr" in new_config: new_config["sonarr"] = new_config.pop("sonarr")
if "omdb" in new_config: new_config["omdb"] = new_config.pop("omdb")
if "trakt" in new_config: new_config["trakt"] = new_config.pop("trakt") if "trakt" in new_config: new_config["trakt"] = new_config.pop("trakt")
if "mal" in new_config: new_config["mal"] = new_config.pop("mal") if "mal" in new_config: new_config["mal"] = new_config.pop("mal")
yaml.round_trip_dump(new_config, open(self.config_path, "w"), indent=ind, block_seq_indent=bsi) yaml.round_trip_dump(new_config, open(self.config_path, "w"), indent=ind, block_seq_indent=bsi)
@ -170,6 +174,21 @@ class Config:
util.separator() util.separator()
self.OMDb = None
if "omdb" in self.data:
logger.info("Connecting to OMDb...")
self.omdb = {}
try:
self.omdb["apikey"] = check_for_attribute(self.data, "apikey", parent="omdb", throw=True)
self.OMDb = OMDbAPI(self.omdb, Cache=self.Cache)
except Failed as e:
logger.error(e)
logger.info(f"OMDb Connection {'Failed' if self.OMDb is None else 'Successful'}")
else:
logger.warning("omdb attribute not found")
util.separator()
self.Trakt = None self.Trakt = None
if "trakt" in self.data: if "trakt" in self.data:
logger.info("Connecting to Trakt...") logger.info("Connecting to Trakt...")
@ -205,9 +224,10 @@ class Config:
else: else:
logger.warning("mal attribute not found") logger.warning("mal attribute not found")
self.TVDb = TVDbAPI(Cache=self.Cache, TMDb=self.TMDb, Trakt=self.Trakt) self.TVDb = TVDbAPI(self)
self.IMDb = IMDbAPI(Cache=self.Cache, TMDb=self.TMDb, Trakt=self.Trakt, TVDb=self.TVDb) if self.TMDb or self.Trakt else None self.IMDb = IMDbAPI(self)
self.AniDB = AniDBAPI(Cache=self.Cache, TMDb=self.TMDb, Trakt=self.Trakt) self.AniDB = AniDBAPI(self)
self.Letterboxd = LetterboxdAPI()
util.separator() util.separator()
@ -260,11 +280,39 @@ class Config:
if params["asset_directory"] is None: if params["asset_directory"] is None:
logger.warning("Config Warning: Assets will not be used asset_directory attribute must be set under config or under this specific Library") logger.warning("Config Warning: Assets will not be used asset_directory attribute must be set under config or under this specific Library")
params["sync_mode"] = check_for_attribute(libs[lib], "sync_mode", parent="settings", test_list=["append", "sync"], options=" append (Only Add Items to the Collection)\n sync (Add & Remove Items from the Collection)", default=self.general["sync_mode"], save=False) if "settings" in libs[lib] and libs[lib]["settings"] and "sync_mode" in libs[lib]["settings"]:
params["show_unmanaged"] = check_for_attribute(libs[lib], "show_unmanaged", parent="settings", var_type="bool", default=self.general["show_unmanaged"], save=False) params["sync_mode"] = check_for_attribute(libs[lib], "sync_mode", parent="settings", test_list=["append", "sync"], options=" append (Only Add Items to the Collection)\n sync (Add & Remove Items from the Collection)", default=self.general["sync_mode"], do_print=False, save=False)
params["show_filtered"] = check_for_attribute(libs[lib], "show_filtered", parent="settings", var_type="bool", default=self.general["show_filtered"], save=False) else:
params["show_missing"] = check_for_attribute(libs[lib], "show_missing", parent="settings", var_type="bool", default=self.general["show_missing"], save=False) params["sync_mode"] = check_for_attribute(libs[lib], "sync_mode", test_list=["append", "sync"], options=" append (Only Add Items to the Collection)\n sync (Add & Remove Items from the Collection)", default=self.general["sync_mode"], do_print=False, save=False)
params["save_missing"] = check_for_attribute(libs[lib], "save_missing", parent="settings", var_type="bool", default=self.general["save_missing"], save=False)
if "settings" in libs[lib] and libs[lib]["settings"] and "show_unmanaged" in libs[lib]["settings"]:
params["show_unmanaged"] = check_for_attribute(libs[lib], "show_unmanaged", parent="settings", var_type="bool", default=self.general["show_unmanaged"], do_print=False, save=False)
else:
params["show_unmanaged"] = check_for_attribute(libs[lib], "show_unmanaged", var_type="bool", default=self.general["show_unmanaged"], do_print=False, save=False)
if "settings" in libs[lib] and libs[lib]["settings"] and "show_filtered" in libs[lib]["settings"]:
params["show_filtered"] = check_for_attribute(libs[lib], "show_filtered", parent="settings", var_type="bool", default=self.general["show_filtered"], do_print=False, save=False)
else:
params["show_filtered"] = check_for_attribute(libs[lib], "show_filtered", var_type="bool", default=self.general["show_filtered"], do_print=False, save=False)
if "settings" in libs[lib] and libs[lib]["settings"] and "show_missing" in libs[lib]["settings"]:
params["show_missing"] = check_for_attribute(libs[lib], "show_missing", parent="settings", var_type="bool", default=self.general["show_missing"], do_print=False, save=False)
else:
params["show_missing"] = check_for_attribute(libs[lib], "show_missing", var_type="bool", default=self.general["show_missing"], do_print=False, save=False)
if "settings" in libs[lib] and libs[lib]["settings"] and "save_missing" in libs[lib]["settings"]:
params["save_missing"] = check_for_attribute(libs[lib], "save_missing", parent="settings", var_type="bool", default=self.general["save_missing"], do_print=False, save=False)
else:
params["save_missing"] = check_for_attribute(libs[lib], "save_missing", var_type="bool", default=self.general["save_missing"], do_print=False, save=False)
if "mass_genre_update" in libs[lib] and libs[lib]["mass_genre_update"]:
params["mass_genre_update"] = check_for_attribute(libs[lib], "mass_genre_update", test_list=["tmdb", "omdb"], options=" tmdb (Use TMDb Metadata)\n omdb (Use IMDb Metadata through OMDb)", default_is_none=True, save=False)
else:
params["mass_genre_update"] = None
if params["mass_genre_update"] == "omdb" and self.OMDb is None:
params["mass_genre_update"] = None
logger.error("Config Error: mass_genre_update cannot be omdb without a successful OMDb Connection")
try: try:
params["metadata_path"] = check_for_attribute(libs[lib], "metadata_path", var_type="path", default=os.path.join(default_dir, f"{lib}.yml"), throw=True) params["metadata_path"] = check_for_attribute(libs[lib], "metadata_path", var_type="path", default=os.path.join(default_dir, f"{lib}.yml"), throw=True)
@ -292,7 +340,7 @@ class Config:
radarr_params["add"] = check_for_attribute(libs[lib], "add", parent="radarr", var_type="bool", default=self.general["radarr"]["add"], save=False) radarr_params["add"] = check_for_attribute(libs[lib], "add", parent="radarr", var_type="bool", default=self.general["radarr"]["add"], save=False)
radarr_params["search"] = check_for_attribute(libs[lib], "search", parent="radarr", var_type="bool", default=self.general["radarr"]["search"], save=False) radarr_params["search"] = check_for_attribute(libs[lib], "search", parent="radarr", var_type="bool", default=self.general["radarr"]["search"], save=False)
radarr_params["tag"] = check_for_attribute(libs[lib], "search", parent="radarr", var_type="lower_list", default=self.general["radarr"]["tag"], default_is_none=True, save=False) radarr_params["tag"] = check_for_attribute(libs[lib], "search", parent="radarr", var_type="lower_list", default=self.general["radarr"]["tag"], default_is_none=True, save=False)
library.add_Radarr(RadarrAPI(self.TMDb, radarr_params)) library.Radarr = RadarrAPI(self.TMDb, radarr_params)
except Failed as e: except Failed as e:
util.print_multiline(e) util.print_multiline(e)
logger.info(f"{params['name']} library's Radarr Connection {'Failed' if library.Radarr is None else 'Successful'}") logger.info(f"{params['name']} library's Radarr Connection {'Failed' if library.Radarr is None else 'Successful'}")
@ -310,7 +358,7 @@ class Config:
sonarr_params["search"] = check_for_attribute(libs[lib], "search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["search"], save=False) sonarr_params["search"] = check_for_attribute(libs[lib], "search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["search"], save=False)
sonarr_params["season_folder"] = check_for_attribute(libs[lib], "season_folder", parent="sonarr", var_type="bool", default=self.general["sonarr"]["season_folder"], save=False) sonarr_params["season_folder"] = check_for_attribute(libs[lib], "season_folder", parent="sonarr", var_type="bool", default=self.general["sonarr"]["season_folder"], save=False)
sonarr_params["tag"] = check_for_attribute(libs[lib], "search", parent="sonarr", var_type="lower_list", default=self.general["sonarr"]["tag"], default_is_none=True, save=False) sonarr_params["tag"] = check_for_attribute(libs[lib], "search", parent="sonarr", var_type="lower_list", default=self.general["sonarr"]["tag"], default_is_none=True, save=False)
library.add_Sonarr(SonarrAPI(self.TVDb, sonarr_params, library.Plex.language)) library.Sonarr = SonarrAPI(self.TVDb, sonarr_params, library.Plex.language)
except Failed as e: except Failed as e:
util.print_multiline(e) util.print_multiline(e)
logger.info(f"{params['name']} library's Sonarr Connection {'Failed' if library.Sonarr is None else 'Successful'}") logger.info(f"{params['name']} library's Sonarr Connection {'Failed' if library.Sonarr is None else 'Successful'}")
@ -321,7 +369,7 @@ class Config:
try: try:
tautulli_params["url"] = check_for_attribute(libs[lib], "url", parent="tautulli", default=self.general["tautulli"]["url"], req_default=True, save=False) tautulli_params["url"] = check_for_attribute(libs[lib], "url", parent="tautulli", default=self.general["tautulli"]["url"], req_default=True, save=False)
tautulli_params["apikey"] = check_for_attribute(libs[lib], "apikey", parent="tautulli", default=self.general["tautulli"]["apikey"], req_default=True, save=False) tautulli_params["apikey"] = check_for_attribute(libs[lib], "apikey", parent="tautulli", default=self.general["tautulli"]["apikey"], req_default=True, save=False)
library.add_Tautulli(TautulliAPI(tautulli_params)) library.Tautulli = TautulliAPI(tautulli_params)
except Failed as e: except Failed as e:
util.print_multiline(e) util.print_multiline(e)
logger.info(f"{params['name']} library's Tautulli Connection {'Failed' if library.Tautulli is None else 'Successful'}") logger.info(f"{params['name']} library's Tautulli Connection {'Failed' if library.Tautulli is None else 'Successful'}")
@ -342,16 +390,19 @@ class Config:
os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(library.timeout) os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(library.timeout)
logger.info("") logger.info("")
util.separator(f"{library.name} Library") util.separator(f"{library.name} Library")
logger.info("")
util.separator(f"Mapping {library.name} Library")
logger.info("")
movie_map, show_map = self.map_guids(library)
if not test:
if library.mass_genre_update:
self.mass_metadata(library, movie_map, show_map)
try: library.update_metadata(self.TMDb, test) try: library.update_metadata(self.TMDb, test)
except Failed as e: logger.error(e) except Failed as e: logger.error(e)
logger.info("") logger.info("")
util.separator(f"{library.name} Library {'Test ' if test else ''}Collections") util.separator(f"{library.name} Library {'Test ' if test else ''}Collections")
collections = {c: library.collections[c] for c in util.get_list(requested_collections) if c in library.collections} if requested_collections else library.collections collections = {c: library.collections[c] for c in util.get_list(requested_collections) if c in library.collections} if requested_collections else library.collections
if collections: if collections:
logger.info("")
util.separator(f"Mapping {library.name} Library")
logger.info("")
movie_map, show_map = self.map_guids(library)
for c in collections: for c in collections:
if test and ("test" not in collections[c] or collections[c]["test"] is not True): if test and ("test" not in collections[c] or collections[c]["test"] is not True):
no_template_test = True no_template_test = True
@ -475,7 +526,119 @@ class Config:
except Failed as e: except Failed as e:
util.print_multiline(e, error=True) util.print_multiline(e, error=True)
continue continue
builder.run_collections_again(library, collection_obj, movie_map, show_map) builder.run_collections_again(collection_obj, movie_map, show_map)
def convert_from_imdb(self, imdb_id, language):
update_tmdb = False
update_tvdb = False
if self.Cache:
tmdb_id, tvdb_id = self.Cache.get_ids_from_imdb(imdb_id)
update_tmdb = False
if not tmdb_id:
tmdb_id, update_tmdb = self.Cache.get_tmdb_from_imdb(imdb_id)
if update_tmdb:
tmdb_id = None
update_tvdb = False
if not tvdb_id:
tvdb_id, update_tvdb = self.Cache.get_tvdb_from_imdb(imdb_id)
if update_tvdb:
tvdb_id = None
else:
tmdb_id = None
tvdb_id = None
from_cache = tmdb_id is not None or tvdb_id is not None
if not tmdb_id and not tvdb_id and self.TMDb:
try:
tmdb_id = self.TMDb.convert_imdb_to_tmdb(imdb_id)
except Failed:
pass
if not tmdb_id and not tvdb_id and self.TMDb:
try:
tvdb_id = self.TMDb.convert_imdb_to_tvdb(imdb_id)
except Failed:
pass
if not tmdb_id and not tvdb_id and self.Trakt:
try:
tmdb_id = self.Trakt.convert_imdb_to_tmdb(imdb_id)
except Failed:
pass
if not tmdb_id and not tvdb_id and self.Trakt:
try:
tvdb_id = self.Trakt.convert_imdb_to_tvdb(imdb_id)
except Failed:
pass
try:
if tmdb_id and not from_cache: self.TMDb.get_movie(tmdb_id)
except Failed: tmdb_id = None
try:
if tvdb_id and not from_cache: self.TVDb.get_series(language, tvdb_id)
except Failed: tvdb_id = None
if not tmdb_id and not tvdb_id: raise Failed(f"IMDb Error: No TMDb ID or TVDb ID found for IMDb: {imdb_id}")
if self.Cache:
if tmdb_id and update_tmdb is not False:
self.Cache.update_imdb("movie", update_tmdb, imdb_id, tmdb_id)
if tvdb_id and update_tvdb is not False:
self.Cache.update_imdb("show", update_tvdb, imdb_id, tvdb_id)
return tmdb_id, tvdb_id
def mass_metadata(self, library, movie_map, show_map):
length = 0
logger.info("")
util.separator(f"Mass Editing {'Movie' if library.is_movie else 'Show'} Library: {library.name}")
logger.info("")
items = library.Plex.all()
for i, item in enumerate(items, 1):
length = util.print_return(length, f"Processing: {i}/{len(items)} {item.title}")
ids = {}
if self.Cache:
ids, expired = self.Cache.get_ids("movie" if library.is_movie else "show", plex_guid=item.guid)
elif library.is_movie:
for tmdb in movie_map:
if movie_map[tmdb] == item.ratingKey:
ids["tmdb"] = tmdb
break
else:
for tvdb in show_map:
if show_map[tvdb] == item.ratingKey:
ids["tvdb"] = tvdb
break
if library.mass_genre_update:
if library.mass_genre_update == "tmdb":
if "tmdb" not in ids:
util.print_end(length, f"{item.title[:25]:<25} | No TMDb for Guid: {item.guid}")
continue
try:
tmdb_item = self.TMDb.get_movie(ids["tmdb"]) if library.is_movie else self.TMDb.get_show(ids["tmdb"])
except Failed as e:
util.print_end(length, str(e))
continue
new_genres = [genre.name for genre in tmdb_item.genres]
elif library.mass_genre_update == "omdb":
if self.OMDb.limit is True:
break
if "imdb" not in ids:
util.print_end(length, f"{item.title[:25]:<25} | No IMDb for Guid: {item.guid}")
continue
try:
omdb_item = self.OMDb.get_omdb(ids["imdb"])
except Failed as e:
util.print_end(length, str(e))
continue
new_genres = omdb_item.genres
else:
raise Failed
item_genres = [genre.tag for genre in item.genres]
display_str = ""
for genre in (g for g in item_genres if g not in new_genres):
item.removeGenre(genre)
display_str += f"{', ' if len(display_str) > 0 else ''}-{genre}"
for genre in (g for g in new_genres if g not in item_genres):
item.addGenre(genre)
display_str += f"{', ' if len(display_str) > 0 else ''}+{genre}"
if len(display_str) > 0:
util.print_end(length, f"{item.title[:25]:<25} | Genres | {display_str}")
def map_guids(self, library): def map_guids(self, library):
movie_map = {} movie_map = {}
@ -521,11 +684,18 @@ class Config:
item_type = guid.scheme.split(".")[-1] item_type = guid.scheme.split(".")[-1]
check_id = guid.netloc check_id = guid.netloc
if item_type == "plex" and library.is_movie: if item_type == "plex" and check_id == "movie":
for guid_tag in item.guids: for guid_tag in item.guids:
url_parsed = requests.utils.urlparse(guid_tag.id) url_parsed = requests.utils.urlparse(guid_tag.id)
if url_parsed.scheme == "tmdb": tmdb_id = int(url_parsed.netloc) if url_parsed.scheme == "tmdb": tmdb_id = int(url_parsed.netloc)
elif url_parsed.scheme == "imdb": imdb_id = url_parsed.netloc elif url_parsed.scheme == "imdb": imdb_id = url_parsed.netloc
elif item_type == "plex" and check_id == "show":
item.reload()
for guid_tag in item.findItems(item._data, Guid):
url_parsed = requests.utils.urlparse(guid_tag.id)
if url_parsed.scheme == "tvdb": tvdb_id = int(url_parsed.netloc)
elif url_parsed.scheme == "imdb": imdb_id = url_parsed.netloc
elif url_parsed.scheme == "tmdb": tmdb_id = int(url_parsed.netloc)
elif item_type == "imdb": imdb_id = check_id elif item_type == "imdb": imdb_id = check_id
elif item_type == "thetvdb": tvdb_id = int(check_id) elif item_type == "thetvdb": tvdb_id = int(check_id)
elif item_type == "themoviedb": tmdb_id = int(check_id) elif item_type == "themoviedb": tmdb_id = int(check_id)
@ -620,7 +790,7 @@ class Config:
elif id_name and api_name: error_message = f"Unable to convert {id_name} to {service_name} using {api_name}" elif id_name and api_name: error_message = f"Unable to convert {id_name} to {service_name} using {api_name}"
elif id_name: error_message = f"Configure TMDb or Trakt to covert {id_name} to {service_name}" elif id_name: error_message = f"Configure TMDb or Trakt to covert {id_name} to {service_name}"
else: error_message = f"No ID to convert to {service_name}" else: error_message = f"No ID to convert to {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)):
if isinstance(tmdb_id, list): if isinstance(tmdb_id, list):
for i in range(len(tmdb_id)): for i in range(len(tmdb_id)):
util.print_end(length, f"Cache | {'^' if expired is True else '+'} | {item.guid:<46} | {tmdb_id[i] if tmdb_id[i] else 'None':<6} | {imdb_id[i] if imdb_id[i] else 'None':<10} | {tvdb_id if tvdb_id else 'None':<6} | {anidb_id if anidb_id else 'None':<5} | {mal_id if mal_id else 'None':<5} | {item.title}") util.print_end(length, f"Cache | {'^' if expired is True else '+'} | {item.guid:<46} | {tmdb_id[i] if tmdb_id[i] else 'None':<6} | {imdb_id[i] if imdb_id[i] else 'None':<10} | {tvdb_id if tvdb_id else 'None':<6} | {anidb_id if anidb_id else 'None':<5} | {mal_id if mal_id else 'None':<5} | {item.title}")

@ -7,23 +7,22 @@ from retrying import retry
logger = logging.getLogger("Plex Meta Manager") logger = logging.getLogger("Plex Meta Manager")
class IMDbAPI: class IMDbAPI:
def __init__(self, Cache=None, TMDb=None, Trakt=None, TVDb=None): def __init__(self, config):
if TMDb is None and Trakt is None: self.config = config
raise Failed("IMDb Error: IMDb requires either TMDb or Trakt") self.urls = {
self.Cache = Cache "list": "https://www.imdb.com/list/ls",
self.TMDb = TMDb "search": "https://www.imdb.com/search/title/?"
self.Trakt = Trakt }
self.TVDb = TVDb
def get_imdb_ids_from_url(self, imdb_url, language, limit): def get_imdb_ids_from_url(self, imdb_url, language, limit):
imdb_url = imdb_url.strip() imdb_url = imdb_url.strip()
if not imdb_url.startswith("https://www.imdb.com/list/ls") and not imdb_url.startswith("https://www.imdb.com/search/title/?"): if not imdb_url.startswith(self.urls["list"]) and not imdb_url.startswith(self.urls["search"]):
raise Failed(f"IMDb Error: {imdb_url} must begin with either:\n| https://www.imdb.com/list/ls (For Lists)\n| https://www.imdb.com/search/title/? (For Searches)") raise Failed(f"IMDb Error: {imdb_url} must begin with either:\n| {self.urls['list']} (For Lists)\n| {self.urls['search']} (For Searches)")
if imdb_url.startswith("https://www.imdb.com/list/ls"): if imdb_url.startswith(self.urls["list"]):
try: list_id = re.search("(\\d+)", str(imdb_url)).group(1) try: list_id = re.search("(\\d+)", str(imdb_url)).group(1)
except AttributeError: raise Failed(f"IMDb Error: Failed to parse List ID from {imdb_url}") except AttributeError: raise Failed(f"IMDb Error: Failed to parse List ID from {imdb_url}")
current_url = f"https://www.imdb.com/search/title/?lists=ls{list_id}" current_url = f"{self.urls['search']}lists=ls{list_id}"
else: else:
current_url = imdb_url current_url = imdb_url
header = {"Accept-Language": language} header = {"Accept-Language": language}
@ -61,7 +60,7 @@ class IMDbAPI:
if method == "imdb_id": if method == "imdb_id":
if status_message: if status_message:
logger.info(f"Processing {pretty}: {data}") logger.info(f"Processing {pretty}: {data}")
tmdb_id, tvdb_id = self.convert_from_imdb(data, language) tmdb_id, tvdb_id = self.config.convert_from_imdb(data, language)
if tmdb_id: movie_ids.append(tmdb_id) if tmdb_id: movie_ids.append(tmdb_id)
if tvdb_id: show_ids.append(tvdb_id) if tvdb_id: show_ids.append(tvdb_id)
elif method == "imdb_list": elif method == "imdb_list":
@ -74,7 +73,7 @@ class IMDbAPI:
for i, imdb_id in enumerate(imdb_ids, 1): for i, imdb_id in enumerate(imdb_ids, 1):
length = util.print_return(length, f"Converting IMDb ID {i}/{total_ids}") length = util.print_return(length, f"Converting IMDb ID {i}/{total_ids}")
try: try:
tmdb_id, tvdb_id = self.convert_from_imdb(imdb_id, language) tmdb_id, tvdb_id = self.config.convert_from_imdb(imdb_id, language)
if tmdb_id: movie_ids.append(tmdb_id) if tmdb_id: movie_ids.append(tmdb_id)
if tvdb_id: show_ids.append(tvdb_id) if tvdb_id: show_ids.append(tvdb_id)
except Failed as e: logger.warning(e) except Failed as e: logger.warning(e)
@ -85,49 +84,3 @@ class IMDbAPI:
logger.debug(f"TMDb IDs Found: {movie_ids}") logger.debug(f"TMDb IDs Found: {movie_ids}")
logger.debug(f"TVDb IDs Found: {show_ids}") logger.debug(f"TVDb IDs Found: {show_ids}")
return movie_ids, show_ids return movie_ids, show_ids
def convert_from_imdb(self, imdb_id, language):
update_tmdb = False
update_tvdb = False
if self.Cache:
tmdb_id, tvdb_id = self.Cache.get_ids_from_imdb(imdb_id)
update_tmdb = False
if not tmdb_id:
tmdb_id, update_tmdb = self.Cache.get_tmdb_from_imdb(imdb_id)
if update_tmdb:
tmdb_id = None
update_tvdb = False
if not tvdb_id:
tvdb_id, update_tvdb = self.Cache.get_tvdb_from_imdb(imdb_id)
if update_tvdb:
tvdb_id = None
else:
tmdb_id = None
tvdb_id = None
from_cache = tmdb_id is not None or tvdb_id is not None
if not tmdb_id and not tvdb_id and self.TMDb:
try: tmdb_id = self.TMDb.convert_imdb_to_tmdb(imdb_id)
except Failed: pass
if not tmdb_id and not tvdb_id and self.TMDb:
try: tvdb_id = self.TMDb.convert_imdb_to_tvdb(imdb_id)
except Failed: pass
if not tmdb_id and not tvdb_id and self.Trakt:
try: tmdb_id = self.Trakt.convert_imdb_to_tmdb(imdb_id)
except Failed: pass
if not tmdb_id and not tvdb_id and self.Trakt:
try: tvdb_id = self.Trakt.convert_imdb_to_tvdb(imdb_id)
except Failed: pass
try:
if tmdb_id and not from_cache: self.TMDb.get_movie(tmdb_id)
except Failed: tmdb_id = None
try:
if tvdb_id and not from_cache: self.TVDb.get_series(language, tvdb_id=tvdb_id)
except Failed: tvdb_id = None
if not tmdb_id and not tvdb_id: raise Failed(f"IMDb Error: No TMDb ID or TVDb ID found for IMDb: {imdb_id}")
if self.Cache:
if tmdb_id and update_tmdb is not False:
self.Cache.update_imdb("movie", update_tmdb, imdb_id, tmdb_id)
if tvdb_id and update_tvdb is not False:
self.Cache.update_imdb("show", update_tvdb, imdb_id, tvdb_id)
return tmdb_id, tvdb_id

@ -0,0 +1,58 @@
import logging, math, re, requests
from lxml import html
from modules import util
from modules.util import Failed
from retrying import retry
logger = logging.getLogger("Plex Meta Manager")
class LetterboxdAPI:
def __init__(self):
self.url = "https://letterboxd.com"
@retry(stop_max_attempt_number=6, wait_fixed=10000)
def send_request(self, url, language):
return html.fromstring(requests.get(url, header={"Accept-Language": language, "User-Agent": "Mozilla/5.0 x64"}).content)
def get_list_description(self, list_url, language):
descriptions = self.send_request(list_url, language).xpath("//meta[@property='og:description']/@content")
return descriptions[0] if len(descriptions) > 0 and len(descriptions[0]) > 0 else None
def parse_list_for_slugs(self, list_url, language):
response = self.send_request(list_url, language)
slugs = response.xpath("//div[@class='poster film-poster really-lazy-load']/@data-film-slug")
next_url = response.xpath("//a[@class='next']/@href")
if len(next_url) > 0:
slugs.extend(self.parse_list_for_slugs(f"{self.url}{next_url[0]}", language))
return slugs
def get_tmdb_from_slug(self, slug, language):
return self.get_tmdb(f"{self.url}{slug}", language)
def get_tmdb(self, letterboxd_url, language):
response = self.send_request(letterboxd_url, language)
ids = response.xpath("//body/@data-tmdb-id")
if len(ids) > 0:
return int(ids[0])
raise Failed(f"Letterboxd Error: TMDb ID not found at {letterboxd_url}")
def get_items(self, method, data, language, status_message=True):
pretty = util.pretty_names[method] if method in util.pretty_names else method
movie_ids = []
if status_message:
logger.info(f"Processing {pretty}: {data}")
slugs = self.parse_list_for_slugs(data, language)
total_slugs = len(slugs)
if total_slugs == 0:
raise Failed(f"Letterboxd Error: No List Items found in {data}")
length = 0
for i, slug in enumerate(slugs, 1):
length = util.print_return(length, f"Finding TMDb ID {i}/{total_slugs}")
try:
movie_ids.append(self.get_tmdb(slug, language))
except Failed as e:
logger.error(e)
util.print_end(length, f"Processed {total_slugs} TMDb IDs")
if status_message:
logger.debug(f"TMDb IDs Found: {movie_ids}")
return movie_ids, []

@ -0,0 +1,60 @@
import logging, math, re, requests
from lxml import html
from modules import util
from modules.util import Failed
from retrying import retry
logger = logging.getLogger("Plex Meta Manager")
class OMDbObj:
def __init__(self, data):
self._data = data
self.title = data["Title"]
try:
self.year = int(data["Year"])
except (ValueError, TypeError):
self.year = None
self.content_rating = data["Rated"]
self.genres = util.get_list(data["Genre"])
self.genres_str = data["Genre"]
try:
self.imdb_rating = float(data["imdbRating"])
except (ValueError, TypeError):
self.imdb_rating = None
try:
self.imdb_votes = int(str(data["imdbVotes"]).replace(',', ''))
except (ValueError, TypeError):
self.imdb_votes = None
try:
self.metacritic_rating = int(data["Metascore"])
except (ValueError, TypeError):
self.metacritic_rating = None
self.imdb_id = data["imdbID"]
self.type = data["Type"]
class OMDbAPI:
def __init__(self, params, Cache=None):
self.url = "http://www.omdbapi.com/"
self.apikey = params["apikey"]
self.limit = False
self.Cache = Cache
self.get_omdb("tt0080684")
@retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed)
def get_omdb(self, imdb_id):
expired = None
if self.Cache:
omdb_dict, expired = self.Cache.query_omdb(imdb_id)
if omdb_dict and expired is False:
return OMDbObj(omdb_dict)
response = requests.get(self.url, params={"i": imdb_id, "apikey": self.apikey})
if response.status_code < 400:
omdb = OMDbObj(response.json())
if self.Cache:
self.Cache.update_omdb(expired, omdb)
return omdb
else:
error = response.json()['Error']
if error == "Request limit reached!":
self.limit = True
raise Failed(f"OMDb Error: {error}")

@ -60,20 +60,12 @@ class PlexAPI:
self.show_filtered = params["show_filtered"] self.show_filtered = params["show_filtered"]
self.show_missing = params["show_missing"] self.show_missing = params["show_missing"]
self.save_missing = params["save_missing"] self.save_missing = params["save_missing"]
self.mass_genre_update = params["mass_genre_update"]
self.plex = params["plex"] self.plex = params["plex"]
self.timeout = params["plex"]["timeout"] self.timeout = params["plex"]["timeout"]
self.missing = {} self.missing = {}
self.run_again = [] self.run_again = []
def add_Radarr(self, Radarr):
self.Radarr = Radarr
def add_Sonarr(self, Sonarr):
self.Sonarr = Sonarr
def add_Tautulli(self, Tautulli):
self.Tautulli = Tautulli
@retry(stop_max_attempt_number=6, wait_fixed=10000) @retry(stop_max_attempt_number=6, wait_fixed=10000)
def search(self, title, libtype=None, year=None): def search(self, title, libtype=None, year=None):
if libtype is not None and year is not None: return self.Plex.search(title=title, year=year, libtype=libtype) if libtype is not None and year is not None: return self.Plex.search(title=title, year=year, libtype=libtype)

@ -57,7 +57,7 @@ class SonarrAPI:
tag_nums.append(tag_cache[label]) tag_nums.append(tag_cache[label])
for tvdb_id in tvdb_ids: for tvdb_id in tvdb_ids:
try: try:
show = self.tvdb.get_series(self.language, tvdb_id=tvdb_id) show = self.tvdb.get_series(self.language, tvdb_id)
except Failed as e: except Failed as e:
logger.error(e) logger.error(e)
continue continue

@ -114,7 +114,10 @@ class TMDbAPI:
if credit.media_type == "movie": if credit.media_type == "movie":
movie_ids.append(credit.id) movie_ids.append(credit.id)
elif credit.media_type == "tv": elif credit.media_type == "tv":
show_ids.append(credit.id) try:
show_ids.append(self.convert_tmdb_to_tvdb(credit.id))
except Failed as e:
logger.warning(e)
for credit in actor_credits.crew: for credit in actor_credits.crew:
if crew or \ if crew or \
(director and credit.department == "Directing") or \ (director and credit.department == "Directing") or \
@ -123,7 +126,10 @@ class TMDbAPI:
if credit.media_type == "movie": if credit.media_type == "movie":
movie_ids.append(credit.id) movie_ids.append(credit.id)
elif credit.media_type == "tv": elif credit.media_type == "tv":
show_ids.append(credit.id) try:
show_ids.append(self.convert_tmdb_to_tvdb(credit.id))
except Failed as e:
logger.warning(e)
return movie_ids, show_ids return movie_ids, show_ids
def get_pagenation(self, method, amount, is_movie): def get_pagenation(self, method, amount, is_movie):

@ -105,10 +105,10 @@ class TraktAPI:
@retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed) @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed)
def standard_list(self, data): def standard_list(self, data):
try: items = Trakt[requests.utils.urlparse(data).path].items() try: trakt_list = Trakt[requests.utils.urlparse(data).path].get()
except AttributeError: items = None except AttributeError: trakt_list = None
if items is None: raise Failed("Trakt Error: No List found") if trakt_list is None: raise Failed("Trakt Error: No List found")
else: return items else: return trakt_list
def validate_trakt_list(self, values): def validate_trakt_list(self, values):
trakt_values = [] trakt_values = []
@ -145,7 +145,7 @@ class TraktAPI:
logger.info(f"Processing {pretty}: {data} {media_type}{'' if data == 1 else 's'}") logger.info(f"Processing {pretty}: {data} {media_type}{'' if data == 1 else 's'}")
else: else:
if method == "trakt_watchlist": trakt_items = self.watchlist(data, is_movie) if method == "trakt_watchlist": trakt_items = self.watchlist(data, is_movie)
elif method == "trakt_list": trakt_items = self.standard_list(data) elif method == "trakt_list": trakt_items = self.standard_list(data).items()
else: raise Failed(f"Trakt Error: Method {method} not supported") else: raise Failed(f"Trakt Error: Method {method} not supported")
if status_message: logger.info(f"Processing {pretty}: {data}") if status_message: logger.info(f"Processing {pretty}: {data}")
show_ids = [] show_ids = []

@ -36,6 +36,12 @@ class TVDbObj:
results = response.xpath("//div[@class='row hidden-xs hidden-sm']/div/img/@src") results = response.xpath("//div[@class='row hidden-xs hidden-sm']/div/img/@src")
self.poster_path = results[0] if len(results) > 0 and len(results[0]) > 0 else None self.poster_path = results[0] if len(results) > 0 and len(results[0]) > 0 else None
results = response.xpath("(//h2[@class='mt-4' and text()='Backgrounds']/following::div/a/@href)[1]")
self.background_path = results[0] if len(results) > 0 and len(results[0]) > 0 else None
results = response.xpath("//div[@class='block']/div[not(@style='display:none')]/p/text()")
self.summary = results[0] if len(results) > 0 and len(results[0]) > 0 else None
tmdb_id = None tmdb_id = None
if is_movie: if is_movie:
results = response.xpath("//*[text()='TheMovieDB.com']/@href") results = response.xpath("//*[text()='TheMovieDB.com']/@href")
@ -45,7 +51,7 @@ class TVDbObj:
if not tmdb_id: if not tmdb_id:
results = response.xpath("//*[text()='IMDB']/@href") results = response.xpath("//*[text()='IMDB']/@href")
if len(results) > 0: if len(results) > 0:
try: tmdb_id = TVDb.convert_from_imdb(util.get_id_from_imdb_url(results[0])) try: tmdb_id, _ = TVDb.config.convert_from_imdb(util.get_id_from_imdb_url(results[0]), language)
except Failed as e: logger.error(e) except Failed as e: logger.error(e)
self.tmdb_id = tmdb_id self.tmdb_id = tmdb_id
self.tvdb_url = tvdb_url self.tvdb_url = tvdb_url
@ -54,10 +60,8 @@ class TVDbObj:
self.TVDb = TVDb self.TVDb = TVDb
class TVDbAPI: class TVDbAPI:
def __init__(self, Cache=None, TMDb=None, Trakt=None): def __init__(self, config):
self.Cache = Cache self.config = config
self.TMDb = TMDb
self.Trakt = Trakt
self.site_url = "https://www.thetvdb.com" self.site_url = "https://www.thetvdb.com"
self.alt_site_url = "https://thetvdb.com" self.alt_site_url = "https://thetvdb.com"
self.list_url = f"{self.site_url}/lists/" self.list_url = f"{self.site_url}/lists/"
@ -69,20 +73,27 @@ class TVDbAPI:
self.series_id_url = f"{self.site_url}/dereferrer/series/" self.series_id_url = f"{self.site_url}/dereferrer/series/"
self.movie_id_url = f"{self.site_url}/dereferrer/movie/" self.movie_id_url = f"{self.site_url}/dereferrer/movie/"
def get_series(self, language, tvdb_url=None, tvdb_id=None): def get_movie_or_series(self, language, tvdb_url, is_movie):
if not tvdb_url and not tvdb_id: return self.get_movie(language, tvdb_url) if is_movie else self.get_series(language, tvdb_url)
raise Failed("TVDB Error: get_series requires either tvdb_url or tvdb_id")
elif not tvdb_url and tvdb_id: def get_series(self, language, tvdb_url):
tvdb_url = f"{self.series_id_url}{tvdb_id}" try:
tvdb_url = f"{self.series_id_url}{int(tvdb_url)}"
except ValueError:
pass
return TVDbObj(tvdb_url, language, False, self) return TVDbObj(tvdb_url, language, False, self)
def get_movie(self, language, tvdb_url=None, tvdb_id=None): def get_movie(self, language, tvdb_url):
if not tvdb_url and not tvdb_id: try:
raise Failed("TVDB Error: get_movie requires either tvdb_url or tvdb_id") tvdb_url = f"{self.movie_id_url}{int(tvdb_url)}"
elif not tvdb_url and tvdb_id: except ValueError:
tvdb_url = f"{self.movie_id_url}{tvdb_id}" pass
return TVDbObj(tvdb_url, language, True, self) return TVDbObj(tvdb_url, language, True, self)
def get_list_description(self, tvdb_url, language):
description = self.send_request(tvdb_url, language).xpath("//div[@class='block']/div[not(@style='display:none')]/p/text()")
return description[0] if len(description) > 0 and len(description[0]) > 0 else ""
def get_tvdb_ids_from_url(self, tvdb_url, language): def get_tvdb_ids_from_url(self, tvdb_url, language):
show_ids = [] show_ids = []
movie_ids = [] movie_ids = []
@ -94,11 +105,11 @@ class TVDbAPI:
title = item.xpath(".//div[@class='col-xs-12 col-sm-9 mt-2']//a/text()")[0] title = item.xpath(".//div[@class='col-xs-12 col-sm-9 mt-2']//a/text()")[0]
item_url = item.xpath(".//div[@class='col-xs-12 col-sm-9 mt-2']//a/@href")[0] item_url = item.xpath(".//div[@class='col-xs-12 col-sm-9 mt-2']//a/@href")[0]
if item_url.startswith("/series/"): if item_url.startswith("/series/"):
try: show_ids.append(self.get_series(language, tvdb_url=f"{self.site_url}{item_url}").id) try: show_ids.append(self.get_series(language, f"{self.site_url}{item_url}").id)
except Failed as e: logger.error(f"{e} for series {title}") except Failed as e: logger.error(f"{e} for series {title}")
elif item_url.startswith("/movies/"): elif item_url.startswith("/movies/"):
try: try:
tmdb_id = self.get_movie(language, tvdb_url=f"{self.site_url}{item_url}").tmdb_id tmdb_id = self.get_movie(language, f"{self.site_url}{item_url}").tmdb_id
if tmdb_id: movie_ids.append(tmdb_id) if tmdb_id: movie_ids.append(tmdb_id)
else: raise Failed(f"TVDb Error: TMDb ID not found from TVDb URL: {tvdb_url}") else: raise Failed(f"TVDb Error: TMDb ID not found from TVDb URL: {tvdb_url}")
except Failed as e: except Failed as e:
@ -125,11 +136,9 @@ class TVDbAPI:
if status_message: if status_message:
logger.info(f"Processing {pretty}: {data}") logger.info(f"Processing {pretty}: {data}")
if method == "tvdb_show": if method == "tvdb_show":
try: show_ids.append(self.get_series(language, tvdb_id=int(data)).id) show_ids.append(self.get_series(language, data).id)
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)).id) movie_ids.append(self.get_movie(language, data).id)
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)
@ -140,29 +149,3 @@ class TVDbAPI:
logger.debug(f"TMDb IDs Found: {movie_ids}") logger.debug(f"TMDb IDs Found: {movie_ids}")
logger.debug(f"TVDb IDs Found: {show_ids}") logger.debug(f"TVDb IDs Found: {show_ids}")
return movie_ids, show_ids return movie_ids, show_ids
def convert_from_imdb(self, imdb_id):
update = False
if self.Cache:
tmdb_id, tvdb_id = self.Cache.get_ids_from_imdb(imdb_id)
if not tmdb_id:
tmdb_id, update = self.Cache.get_tmdb_from_imdb(imdb_id)
if update:
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(f"TVDb Error: No TMDb ID found for IMDb: {imdb_id}")
if self.Cache and tmdb_id and update is not False:
self.Cache.update_imdb("movie", update, imdb_id, tmdb_id)
return tmdb_id

@ -97,6 +97,8 @@ pretty_names = {
"anidb_popular": "AniDB Popular", "anidb_popular": "AniDB Popular",
"imdb_list": "IMDb List", "imdb_list": "IMDb List",
"imdb_id": "IMDb ID", "imdb_id": "IMDb ID",
"letterboxd_list": "Letterboxd List",
"letterboxd_list_details": "Letterboxd List",
"mal_id": "MyAnimeList ID", "mal_id": "MyAnimeList ID",
"mal_all": "MyAnimeList All", "mal_all": "MyAnimeList All",
"mal_airing": "MyAnimeList Airing", "mal_airing": "MyAnimeList Airing",
@ -144,11 +146,15 @@ pretty_names = {
"tmdb_writer": "TMDb Writer", "tmdb_writer": "TMDb Writer",
"tmdb_writer_details": "TMDb Writer", "tmdb_writer_details": "TMDb Writer",
"trakt_list": "Trakt List", "trakt_list": "Trakt List",
"trakt_list_details": "Trakt List",
"trakt_trending": "Trakt Trending", "trakt_trending": "Trakt Trending",
"trakt_watchlist": "Trakt Watchlist", "trakt_watchlist": "Trakt Watchlist",
"tvdb_list": "TVDb List", "tvdb_list": "TVDb List",
"tvdb_list_details": "TVDb List",
"tvdb_movie": "TVDb Movie", "tvdb_movie": "TVDb Movie",
"tvdb_show": "TVDb Show" "tvdb_movie_details": "TVDb Movie",
"tvdb_show": "TVDb Show",
"tvdb_show_details": "TVDb Show"
} }
mal_ranked_name = { mal_ranked_name = {
"mal_all": "all", "mal_all": "all",
@ -214,6 +220,8 @@ all_lists = [
"anidb_popular", "anidb_popular",
"imdb_list", "imdb_list",
"imdb_id", "imdb_id",
"letterboxd_list",
"letterboxd_list_details",
"mal_id", "mal_id",
"mal_all", "mal_all",
"mal_airing", "mal_airing",
@ -259,11 +267,15 @@ all_lists = [
"tmdb_writer", "tmdb_writer",
"tmdb_writer_details", "tmdb_writer_details",
"trakt_list", "trakt_list",
"trakt_list_details",
"trakt_trending", "trakt_trending",
"trakt_watchlist", "trakt_watchlist",
"tvdb_list", "tvdb_list",
"tvdb_list_details",
"tvdb_movie", "tvdb_movie",
"tvdb_show" "tvdb_movie_details",
"tvdb_show",
"tvdb_show_details"
] ]
collectionless_lists = [ collectionless_lists = [
"sort_title", "content_rating", "sort_title", "content_rating",
@ -299,6 +311,7 @@ plex_searches = [
"genre", #"genre.not", "genre", #"genre.not",
"producer", #"producer.not", "producer", #"producer.not",
"studio", #"studio.not", "studio", #"studio.not",
"title",
"writer", #"writer.not" "writer", #"writer.not"
"year" #"year.not", "year" #"year.not",
] ]
@ -306,15 +319,19 @@ show_only_lists = [
"tmdb_network", "tmdb_network",
"tmdb_show", "tmdb_show",
"tmdb_show_details", "tmdb_show_details",
"tvdb_show" "tvdb_show",
"tvdb_show_details"
] ]
movie_only_lists = [ movie_only_lists = [
"letterboxd_list",
"letterboxd_list_details",
"tmdb_collection", "tmdb_collection",
"tmdb_collection_details", "tmdb_collection_details",
"tmdb_movie", "tmdb_movie",
"tmdb_movie_details", "tmdb_movie_details",
"tmdb_now_playing", "tmdb_now_playing",
"tvdb_movie" "tvdb_movie",
"tvdb_movie_details"
] ]
movie_only_searches = [ movie_only_searches = [
"actor", "actor.not", "actor", "actor.not",
@ -440,10 +457,10 @@ boolean_details = [
] ]
all_details = [ all_details = [
"sort_title", "content_rating", "sort_title", "content_rating",
"summary", "tmdb_summary", "tmdb_description", "tmdb_biography", "summary", "tmdb_summary", "tmdb_description", "tmdb_biography", "tvdb_summary", "tvdb_description", "trakt_description", "letterboxd_description",
"collection_mode", "collection_order", "collection_mode", "collection_order",
"url_poster", "tmdb_poster", "tmdb_profile", "file_poster", "url_poster", "tmdb_poster", "tmdb_profile", "tvdb_poster", "file_poster",
"url_background", "file_background", "url_background", "tmdb_background", "tvdb_background", "file_background",
"name_mapping", "add_to_arr", "arr_tag", "label", "name_mapping", "add_to_arr", "arr_tag", "label",
"show_filtered", "show_missing", "save_missing" "show_filtered", "show_missing", "save_missing"
] ]

@ -1,7 +1,12 @@
import argparse, logging, os, re, schedule, sys, time import argparse, logging, os, re, sys, time
from datetime import datetime from datetime import datetime
from modules import tests, util try:
from modules.config import Config import schedule
from modules import tests, util
from modules.config import Config
except ModuleNotFoundError:
print("Error: Requirements are not installed")
sys.exit(0)
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("--my-tests", dest="tests", help=argparse.SUPPRESS, action="store_true", default=False) parser.add_argument("--my-tests", dest="tests", help=argparse.SUPPRESS, action="store_true", default=False)
@ -60,7 +65,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.3.0 ")) logger.info(util.get_centered_text(" Version: 1.4.0 "))
util.separator() util.separator()
if args.tests: if args.tests:

Loading…
Cancel
Save