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
#### 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 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
@ -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)
* 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)
* [Buy Me a Pizza](https://www.buymeacoffee.com/meisnate12)

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

@ -7,10 +7,8 @@ from retrying import retry
logger = logging.getLogger("Plex Meta Manager")
class AniDBAPI:
def __init__(self, Cache=None, TMDb=None, Trakt=None):
self.Cache = Cache
self.TMDb = TMDb
self.Trakt = Trakt
def __init__(self, config):
self.config = config
self.urls = {
"anime": "https://anidb.net/anime",
"popular": "https://anidb.net/latest/anime/popular/?h=1",
@ -80,7 +78,8 @@ class AniDBAPI:
movie_ids = []
for anidb_id in anime_ids:
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)
else: raise Failed
except Failed:
@ -91,36 +90,3 @@ class AniDBAPI:
logger.debug(f"TMDb IDs Found: {movie_ids}")
logger.debug(f"TVDb IDs Found: {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
elif method_name == "tmdb_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":
if data[m] in ["default", "hide", "hide_items", "show_items", "hideItems", "showItems"]:
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}"
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}"
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":
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])}")
@ -265,6 +275,8 @@ class CollectionBuilder:
self.backgrounds[method_name] = data[m]
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}"
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":
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])}")
@ -294,6 +306,8 @@ class CollectionBuilder:
else:
final_values.append(value)
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:
self.methods.append(("plex_search", [[(method_name, util.get_list(data[m]))]]))
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)))
elif method_name == "trakt_list":
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":
self.methods.append((method_name, config.Trakt.validate_trakt_watchlist(util.get_list(data[m]), self.library.is_movie)))
elif method_name == "imdb_list":
@ -327,6 +347,12 @@ class CollectionBuilder:
list_count = 0
new_list.append({"url": imdb_url, "limit": list_count})
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:
if isinstance(data[m], dict):
def get_int(parent, method, data_in, default_in, minimum=1, maximum=None):
@ -402,6 +428,9 @@ class CollectionBuilder:
if len(years) > 0:
used.append(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:
used.append(util.remove_not(search))
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")
list_count = 20
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:
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":
@ -549,8 +602,10 @@ class CollectionBuilder:
self.methods.append((method_name, util.get_list(data[m])))
elif method_name not in util.other_attributes:
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")
else:
logger.warning(f"Collection Warning: {m} attribute is blank")
self.sync = self.library.sync_mode == "sync"
if "sync_mode" in data:
@ -600,17 +655,32 @@ class CollectionBuilder:
items_found += len(items)
elif method == "plex_search":
search_terms = {}
for i, attr_pair in enumerate(value):
search_list = attr_pair[1]
final_method = attr_pair[0][:-4] + "!" if attr_pair[0][-4:] == ".not" else attr_pair[0]
title_search = None
has_processed = False
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:
final_method = "show." + final_method
search_terms[final_method] = search_list
ors = ""
for o, param in enumerate(attr_pair[1]):
or_des = " OR " if o > 0 else f"{attr_pair[0]}("
for o, param in enumerate(search_list):
or_des = " OR " if o > 0 else f"{search_method}("
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_found += len(items)
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 "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 "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 "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")
@ -694,7 +765,7 @@ class CollectionBuilder:
missing_shows_with_names = []
for missing_id in missing_shows:
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:
logger.error(e)
continue
@ -738,10 +809,13 @@ class CollectionBuilder:
return summaries[summary_method]
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 "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 "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_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 "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_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)
@ -749,6 +823,8 @@ class CollectionBuilder:
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_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)
else: summary = None
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))]
if len(dirs) > 0:
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:
matches = glob.glob(os.path.join(path, folder, "poster.*"))
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}")
if poster_path is None and background_path is None:
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:
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 "tmdb_poster" in self.posters: set_image("tmdb_poster", 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 "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_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_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_movie_details" in self.posters: set_image("tmdb_movie", self.posters)
elif "tmdb_show_details" in self.posters: set_image("tmdb_show", self.posters)
elif "tmdb_movie_details" in self.posters: set_image("tmdb_movie_details", 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")
logger.info("")
@ -867,25 +953,28 @@ class CollectionBuilder:
logger.info(f"Method: {b} Background: {self.backgrounds[b]}")
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 "tmdb_background" in self.backgrounds: set_image("tmdb_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_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 "tmdb_collection_details" in self.backgrounds: set_image("tmdb_collection", self.backgrounds, is_background=True)
elif "tmdb_movie_details" in self.backgrounds: set_image("tmdb_movie", self.backgrounds, is_background=True)
elif "tmdb_show_details" in self.backgrounds: set_image("tmdb_show", 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_details", 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")
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 []
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]
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])
if len(rating_keys) > 0:
for rating_key in rating_keys:
try:
current = library.fetchItem(int(rating_key))
current = self.library.fetchItem(int(rating_key))
except (BadRequest, NotFound):
logger.error(f"Plex Error: Item {rating_key} not found")
continue
@ -894,7 +983,7 @@ class CollectionBuilder:
else:
current.addCollection(name)
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:
logger.info("")
@ -910,12 +999,12 @@ class CollectionBuilder:
logger.info("")
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("")
for missing_id in self.missing_shows:
if missing_id not in show_map:
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:
logger.error(e)
continue

@ -1,6 +1,7 @@
import logging, os, random, sqlite3
from contextlib import closing
from datetime import datetime, timedelta
from modules.util import Failed
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'")
if cursor.fetchone()[0] == 0:
logger.info(f"Initializing cache database at {cache}")
else:
logger.info(f"Using cache database at {cache}")
cursor.execute(
"""CREATE TABLE IF NOT EXISTS guids (
INTEGER PRIMARY KEY,
plex_guid TEXT,
plex_guid TEXT UNIQUE,
tmdb_id TEXT,
imdb_id TEXT,
tvdb_id TEXT,
@ -28,13 +31,25 @@ class Cache:
cursor.execute(
"""CREATE TABLE IF NOT EXISTS imdb_map (
INTEGER PRIMARY KEY,
imdb_id TEXT,
imdb_id TEXT UNIQUE,
t_id TEXT,
expiration_date TEXT,
media_type TEXT)"""
)
else:
logger.info(f"Using cache database at {cache}")
cursor.execute(
"""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.cache_path = cache
@ -82,6 +97,40 @@ class Cache:
expired = time_between_insertion.days > self.expiration
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):
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:
@ -126,3 +175,35 @@ class Cache:
with closing(connection.cursor()) as cursor:
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))
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.cache import Cache
from modules.imdb import IMDbAPI
from modules.letterboxd import LetterboxdAPI
from modules.mal import MyAnimeListAPI
from modules.mal import MyAnimeListIDList
from modules.omdb import OMDbAPI
from modules.plex import PlexAPI
from modules.radarr import RadarrAPI
from modules.sonarr import SonarrAPI
@ -15,6 +17,7 @@ from modules.trakttv import TraktAPI
from modules.tvdb import TVDbAPI
from modules.util import Failed
from plexapi.exceptions import BadRequest
from plexapi.media import Guid
from ruamel import yaml
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 "radarr" in new_config: new_config["radarr"] = new_config.pop("radarr")
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 "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)
@ -170,6 +174,21 @@ class Config:
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
if "trakt" in self.data:
logger.info("Connecting to Trakt...")
@ -205,9 +224,10 @@ class Config:
else:
logger.warning("mal attribute not found")
self.TVDb = TVDbAPI(Cache=self.Cache, TMDb=self.TMDb, Trakt=self.Trakt)
self.IMDb = IMDbAPI(Cache=self.Cache, TMDb=self.TMDb, Trakt=self.Trakt, TVDb=self.TVDb) if self.TMDb or self.Trakt else None
self.AniDB = AniDBAPI(Cache=self.Cache, TMDb=self.TMDb, Trakt=self.Trakt)
self.TVDb = TVDbAPI(self)
self.IMDb = IMDbAPI(self)
self.AniDB = AniDBAPI(self)
self.Letterboxd = LetterboxdAPI()
util.separator()
@ -260,11 +280,39 @@ class Config:
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")
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)
params["show_unmanaged"] = check_for_attribute(libs[lib], "show_unmanaged", parent="settings", var_type="bool", default=self.general["show_unmanaged"], save=False)
params["show_filtered"] = check_for_attribute(libs[lib], "show_filtered", parent="settings", var_type="bool", default=self.general["show_filtered"], save=False)
params["show_missing"] = check_for_attribute(libs[lib], "show_missing", parent="settings", var_type="bool", default=self.general["show_missing"], 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 "sync_mode" in libs[lib]["settings"]:
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)
else:
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)
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:
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["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)
library.add_Radarr(RadarrAPI(self.TMDb, radarr_params))
library.Radarr = RadarrAPI(self.TMDb, radarr_params)
except Failed as e:
util.print_multiline(e)
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["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)
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:
util.print_multiline(e)
logger.info(f"{params['name']} library's Sonarr Connection {'Failed' if library.Sonarr is None else 'Successful'}")
@ -321,7 +369,7 @@ class Config:
try:
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)
library.add_Tautulli(TautulliAPI(tautulli_params))
library.Tautulli = TautulliAPI(tautulli_params)
except Failed as e:
util.print_multiline(e)
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)
logger.info("")
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)
except Failed as e: logger.error(e)
logger.info("")
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
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:
if test and ("test" not in collections[c] or collections[c]["test"] is not True):
no_template_test = True
@ -475,7 +526,119 @@ class Config:
except Failed as e:
util.print_multiline(e, error=True)
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):
movie_map = {}
@ -521,11 +684,18 @@ class Config:
item_type = guid.scheme.split(".")[-1]
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:
url_parsed = requests.utils.urlparse(guid_tag.id)
if url_parsed.scheme == "tmdb": tmdb_id = int(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 == "thetvdb": tvdb_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: 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}"
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):
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}")

@ -7,23 +7,22 @@ from retrying import retry
logger = logging.getLogger("Plex Meta Manager")
class IMDbAPI:
def __init__(self, Cache=None, TMDb=None, Trakt=None, TVDb=None):
if TMDb is None and Trakt is None:
raise Failed("IMDb Error: IMDb requires either TMDb or Trakt")
self.Cache = Cache
self.TMDb = TMDb
self.Trakt = Trakt
self.TVDb = TVDb
def __init__(self, config):
self.config = config
self.urls = {
"list": "https://www.imdb.com/list/ls",
"search": "https://www.imdb.com/search/title/?"
}
def get_imdb_ids_from_url(self, imdb_url, language, limit):
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/?"):
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)")
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| {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)
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:
current_url = imdb_url
header = {"Accept-Language": language}
@ -61,7 +60,7 @@ class IMDbAPI:
if method == "imdb_id":
if status_message:
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 tvdb_id: show_ids.append(tvdb_id)
elif method == "imdb_list":
@ -74,7 +73,7 @@ class IMDbAPI:
for i, imdb_id in enumerate(imdb_ids, 1):
length = util.print_return(length, f"Converting IMDb ID {i}/{total_ids}")
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 tvdb_id: show_ids.append(tvdb_id)
except Failed as e: logger.warning(e)
@ -85,49 +84,3 @@ class IMDbAPI:
logger.debug(f"TMDb IDs Found: {movie_ids}")
logger.debug(f"TVDb IDs Found: {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_missing = params["show_missing"]
self.save_missing = params["save_missing"]
self.mass_genre_update = params["mass_genre_update"]
self.plex = params["plex"]
self.timeout = params["plex"]["timeout"]
self.missing = {}
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)
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)

@ -57,7 +57,7 @@ class SonarrAPI:
tag_nums.append(tag_cache[label])
for tvdb_id in tvdb_ids:
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:
logger.error(e)
continue

@ -114,7 +114,10 @@ class TMDbAPI:
if credit.media_type == "movie":
movie_ids.append(credit.id)
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:
if crew or \
(director and credit.department == "Directing") or \
@ -123,7 +126,10 @@ class TMDbAPI:
if credit.media_type == "movie":
movie_ids.append(credit.id)
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
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)
def standard_list(self, data):
try: items = Trakt[requests.utils.urlparse(data).path].items()
except AttributeError: items = None
if items is None: raise Failed("Trakt Error: No List found")
else: return items
try: trakt_list = Trakt[requests.utils.urlparse(data).path].get()
except AttributeError: trakt_list = None
if trakt_list is None: raise Failed("Trakt Error: No List found")
else: return trakt_list
def validate_trakt_list(self, values):
trakt_values = []
@ -145,7 +145,7 @@ class TraktAPI:
logger.info(f"Processing {pretty}: {data} {media_type}{'' if data == 1 else 's'}")
else:
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")
if status_message: logger.info(f"Processing {pretty}: {data}")
show_ids = []

@ -36,6 +36,12 @@ class TVDbObj:
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
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
if is_movie:
results = response.xpath("//*[text()='TheMovieDB.com']/@href")
@ -45,7 +51,7 @@ class TVDbObj:
if not tmdb_id:
results = response.xpath("//*[text()='IMDB']/@href")
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)
self.tmdb_id = tmdb_id
self.tvdb_url = tvdb_url
@ -54,10 +60,8 @@ class TVDbObj:
self.TVDb = TVDb
class TVDbAPI:
def __init__(self, Cache=None, TMDb=None, Trakt=None):
self.Cache = Cache
self.TMDb = TMDb
self.Trakt = Trakt
def __init__(self, config):
self.config = config
self.site_url = "https://www.thetvdb.com"
self.alt_site_url = "https://thetvdb.com"
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.movie_id_url = f"{self.site_url}/dereferrer/movie/"
def get_series(self, language, tvdb_url=None, tvdb_id=None):
if not tvdb_url and not tvdb_id:
raise Failed("TVDB Error: get_series requires either tvdb_url or tvdb_id")
elif not tvdb_url and tvdb_id:
tvdb_url = f"{self.series_id_url}{tvdb_id}"
def get_movie_or_series(self, language, tvdb_url, is_movie):
return self.get_movie(language, tvdb_url) if is_movie else self.get_series(language, tvdb_url)
def get_series(self, language, tvdb_url):
try:
tvdb_url = f"{self.series_id_url}{int(tvdb_url)}"
except ValueError:
pass
return TVDbObj(tvdb_url, language, False, self)
def get_movie(self, language, tvdb_url=None, tvdb_id=None):
if not tvdb_url and not tvdb_id:
raise Failed("TVDB Error: get_movie requires either tvdb_url or tvdb_id")
elif not tvdb_url and tvdb_id:
tvdb_url = f"{self.movie_id_url}{tvdb_id}"
def get_movie(self, language, tvdb_url):
try:
tvdb_url = f"{self.movie_id_url}{int(tvdb_url)}"
except ValueError:
pass
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):
show_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]
item_url = item.xpath(".//div[@class='col-xs-12 col-sm-9 mt-2']//a/@href")[0]
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}")
elif item_url.startswith("/movies/"):
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)
else: raise Failed(f"TVDb Error: TMDb ID not found from TVDb URL: {tvdb_url}")
except Failed as e:
@ -125,11 +136,9 @@ class TVDbAPI:
if status_message:
logger.info(f"Processing {pretty}: {data}")
if method == "tvdb_show":
try: show_ids.append(self.get_series(language, tvdb_id=int(data)).id)
except ValueError: show_ids.append(self.get_series(language, tvdb_url=data).id)
show_ids.append(self.get_series(language, data).id)
elif method == "tvdb_movie":
try: movie_ids.append(self.get_movie(language, tvdb_id=int(data)).id)
except ValueError: movie_ids.append(self.get_movie(language, tvdb_url=data).id)
movie_ids.append(self.get_movie(language, data).id)
elif method == "tvdb_list":
tmdb_ids, tvdb_ids = self.get_tvdb_ids_from_url(data, language)
movie_ids.extend(tmdb_ids)
@ -140,29 +149,3 @@ class TVDbAPI:
logger.debug(f"TMDb IDs Found: {movie_ids}")
logger.debug(f"TVDb IDs Found: {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",
"imdb_list": "IMDb List",
"imdb_id": "IMDb ID",
"letterboxd_list": "Letterboxd List",
"letterboxd_list_details": "Letterboxd List",
"mal_id": "MyAnimeList ID",
"mal_all": "MyAnimeList All",
"mal_airing": "MyAnimeList Airing",
@ -144,11 +146,15 @@ pretty_names = {
"tmdb_writer": "TMDb Writer",
"tmdb_writer_details": "TMDb Writer",
"trakt_list": "Trakt List",
"trakt_list_details": "Trakt List",
"trakt_trending": "Trakt Trending",
"trakt_watchlist": "Trakt Watchlist",
"tvdb_list": "TVDb List",
"tvdb_list_details": "TVDb List",
"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_all": "all",
@ -214,6 +220,8 @@ all_lists = [
"anidb_popular",
"imdb_list",
"imdb_id",
"letterboxd_list",
"letterboxd_list_details",
"mal_id",
"mal_all",
"mal_airing",
@ -259,11 +267,15 @@ all_lists = [
"tmdb_writer",
"tmdb_writer_details",
"trakt_list",
"trakt_list_details",
"trakt_trending",
"trakt_watchlist",
"tvdb_list",
"tvdb_list_details",
"tvdb_movie",
"tvdb_show"
"tvdb_movie_details",
"tvdb_show",
"tvdb_show_details"
]
collectionless_lists = [
"sort_title", "content_rating",
@ -299,6 +311,7 @@ plex_searches = [
"genre", #"genre.not",
"producer", #"producer.not",
"studio", #"studio.not",
"title",
"writer", #"writer.not"
"year" #"year.not",
]
@ -306,15 +319,19 @@ show_only_lists = [
"tmdb_network",
"tmdb_show",
"tmdb_show_details",
"tvdb_show"
"tvdb_show",
"tvdb_show_details"
]
movie_only_lists = [
"letterboxd_list",
"letterboxd_list_details",
"tmdb_collection",
"tmdb_collection_details",
"tmdb_movie",
"tmdb_movie_details",
"tmdb_now_playing",
"tvdb_movie"
"tvdb_movie",
"tvdb_movie_details"
]
movie_only_searches = [
"actor", "actor.not",
@ -440,10 +457,10 @@ boolean_details = [
]
all_details = [
"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",
"url_poster", "tmdb_poster", "tmdb_profile", "file_poster",
"url_background", "file_background",
"url_poster", "tmdb_poster", "tmdb_profile", "tvdb_poster", "file_poster",
"url_background", "tmdb_background", "tvdb_background", "file_background",
"name_mapping", "add_to_arr", "arr_tag", "label",
"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
try:
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.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(" Version: 1.3.0 "))
logger.info(util.get_centered_text(" Version: 1.4.0 "))
util.separator()
if args.tests:

Loading…
Cancel
Save