Merge pull request #149 from meisnate12/develop

v1.6.4
pull/176/head v1.6.4
meisnate12 4 years ago committed by GitHub
commit 4c8ae0e559
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,5 +1,5 @@
# Plex Meta Manager
#### Version 1.6.2
#### Version 1.6.4
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.

@ -248,7 +248,7 @@ class CollectionBuilder:
elif method_name == "tvdb_description":
self.summaries[method_name] = config.TVDb.get_list_description(method_data, 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(method_data))[0]).description
self.summaries[method_name] = config.Trakt.standard_list(config.Trakt.validate_trakt(util.get_list(method_data))[0]).description
elif method_name == "letterboxd_description":
self.summaries[method_name] = config.Letterboxd.get_list_description(method_data, self.library.Plex.language)
elif method_name == "collection_mode":
@ -266,7 +266,7 @@ class CollectionBuilder:
if str(method_data).lower() == "release":
self.details[method_name] = "release"
elif str(method_data).lower() == "alpha":
self.details[method_name] = "release"
self.details[method_name] = "alpha"
else:
raise Failed(f"Collection Error: {method_data} collection_order invalid\n\trelease (Order Collection by release dates)\n\talpha (Order Collection Alphabetically)")
elif method_name == "url_poster":
@ -309,11 +309,11 @@ class CollectionBuilder:
elif method_name in ["title", "title.and", "title.not", "title.begins", "title.ends"]:
self.methods.append(("plex_search", [{method_name: util.get_list(method_data, split=False)}]))
elif method_name in ["decade", "year.greater", "year.less"]:
self.methods.append(("plex_search", [{method_name: util.check_year(method_data, current_year, method_name)}]))
self.methods.append(("plex_search", [{method_name: [util.check_year(method_data, current_year, method_name)]}]))
elif method_name in ["added.before", "added.after", "originally_available.before", "originally_available.after"]:
self.methods.append(("plex_search", [{method_name: util.check_date(method_data, method_name, return_string=True, plex_date=True)}]))
self.methods.append(("plex_search", [{method_name: [util.check_date(method_data, method_name, return_string=True, plex_date=True)]}]))
elif method_name in ["duration.greater", "duration.less", "rating.greater", "rating.less"]:
self.methods.append(("plex_search", [{method_name: util.check_number(method_data, method_name, minimum=0)}]))
self.methods.append(("plex_search", [{method_name: [util.check_number(method_data, method_name, minimum=0)]}]))
elif method_name in ["year", "year.not"]:
self.methods.append(("plex_search", [{method_name: util.get_year_list(method_data, current_year, method_name)}]))
elif method_name in util.tmdb_searches:
@ -355,15 +355,15 @@ class CollectionBuilder:
elif method_name in ["anilist_id", "anilist_relations", "anilist_studio"]:
self.methods.append((method_name, config.AniList.validate_anilist_ids(util.get_int_list(method_data, "AniList ID"), studio=method_name == "anilist_studio")))
elif method_name == "trakt_list":
self.methods.append((method_name, config.Trakt.validate_trakt_list(util.get_list(method_data))))
self.methods.append((method_name, config.Trakt.validate_trakt(util.get_list(method_data))))
elif method_name == "trakt_list_details":
valid_list = config.Trakt.validate_trakt_list(util.get_list(method_data))
valid_list = config.Trakt.validate_trakt(util.get_list(method_data))
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(method_data), self.library.is_movie)))
elif method_name in ["trakt_watchlist", "trakt_collection"]:
self.methods.append((method_name, config.Trakt.validate_trakt(method_name[6:], util.get_list(method_data), self.library.is_movie)))
elif method_name == "imdb_list":
new_list = []
for imdb_list in util.get_list(method_data, split=False):
@ -492,11 +492,11 @@ class CollectionBuilder:
searches[search_final] = self.library.validate_search_list(final_values, search)
elif (search == "decade" and modifier in [""]) \
or (search == "year" and modifier in [".greater", ".less"]):
searches[search_final] = util.check_year(search_data, current_year, search_final)
searches[search_final] = [util.check_year(search_data, current_year, search_final)]
elif search in ["added", "originally_available"] and modifier in [".before", ".after"]:
searches[search_final] = util.check_date(search_data, search_final, return_string=True, plex_date=True)
searches[search_final] = [util.check_date(search_data, search_final, return_string=True, plex_date=True)]
elif search in ["duration", "rating"] and modifier in [".greater", ".less"]:
searches[search_final] = util.check_number(search_data, search_final, minimum=0)
searches[search_final] = [util.check_number(search_data, search_final, minimum=0)]
elif search == "year" and modifier in ["", ".not"]:
searches[search_final] = util.get_year_list(search_data, current_year, search_final)
elif (search in ["title", "studio"] and modifier not in ["", ".and", ".not", ".begins", ".ends"]) \

@ -99,7 +99,7 @@ class Config:
elif attribute not in loaded_config[parent]: loaded_config[parent][attribute] = default
else: endline = ""
yaml.round_trip_dump(loaded_config, open(self.config_path, "w"), indent=ind_in, block_seq_indent=bsi_in)
elif not data[attribute] and data[attribute] is not False:
elif data[attribute] is None:
if default_is_none is True: return None
else: message = f"{text} is blank"
elif var_type == "bool":
@ -325,7 +325,7 @@ class Config:
library = PlexAPI(params, self.TMDb, self.TVDb)
logger.info(f"{params['name']} Library Connection Successful")
except Failed as e:
logger.error(e)
util.print_multiline(e, error=True)
logger.info(f"{params['name']} Library Connection Failed")
continue
@ -343,7 +343,7 @@ class Config:
radarr_params["tag"] = check_for_attribute(lib, "search", parent="radarr", var_type="lower_list", default=self.general["radarr"]["tag"], default_is_none=True, save=False)
library.Radarr = RadarrAPI(self.TMDb, radarr_params)
except Failed as e:
util.print_multiline(e)
util.print_multiline(e, error=True)
logger.info(f"{params['name']} library's Radarr Connection {'Failed' if library.Radarr is None else 'Successful'}")
if self.general["sonarr"]["url"] or "sonarr" in lib:
@ -361,7 +361,7 @@ class Config:
sonarr_params["tag"] = check_for_attribute(lib, "search", parent="sonarr", var_type="lower_list", default=self.general["sonarr"]["tag"], default_is_none=True, save=False)
library.Sonarr = SonarrAPI(self.TVDb, sonarr_params, library.Plex.language)
except Failed as e:
util.print_multiline(e)
util.print_multiline(e, error=True)
logger.info(f"{params['name']} library's Sonarr Connection {'Failed' if library.Sonarr is None else 'Successful'}")
if self.general["tautulli"]["url"] or "tautulli" in lib:
@ -372,7 +372,7 @@ class Config:
tautulli_params["apikey"] = check_for_attribute(lib, "apikey", parent="tautulli", default=self.general["tautulli"]["apikey"], req_default=True, save=False)
library.Tautulli = TautulliAPI(tautulli_params)
except Failed as e:
util.print_multiline(e)
util.print_multiline(e, error=True)
logger.info(f"{params['name']} library's Tautulli Connection {'Failed' if library.Tautulli is None else 'Successful'}")
self.libraries.append(library)

@ -306,6 +306,13 @@ class PlexAPI:
tagline = tmdb_item.tagline if tmdb_item and len(tmdb_item.tagline) > 0 else None
summary = tmdb_item.overview if tmdb_item else None
details_updated = False
advance_details_updated = False
genre_updated = False
label_updated = False
season_updated = False
episode_updated = False
edits = {}
def add_edit(name, current, group, alias, key=None, value=None):
if value or name in alias:
@ -329,6 +336,7 @@ class PlexAPI:
add_edit("summary", item.summary, meta, methods, value=summary)
if len(edits) > 0:
logger.debug(f"Details Update: {edits}")
details_updated = True
try:
item.edit(**edits)
item.reload()
@ -336,8 +344,6 @@ class PlexAPI:
except BadRequest:
util.print_stacktrace()
logger.error(f"{item_type}: {mapping_name} Details Update Failed")
else:
logger.info(f"{item_type}: {mapping_name} Details Update Not Needed")
advance_edits = {}
if self.is_show:
@ -476,6 +482,7 @@ class PlexAPI:
if len(advance_edits) > 0:
logger.debug(f"Details Update: {advance_edits}")
advance_details_updated = True
try:
check_dict = {pref.id: list(pref.enumValues.keys()) for pref in item.preferences()}
logger.info(check_dict)
@ -484,9 +491,7 @@ class PlexAPI:
logger.info(f"{item_type}: {mapping_name} Advanced Details Update Successful")
except BadRequest:
util.print_stacktrace()
logger.error(f"{item_type}: {mapping_name} Details Update Failed")
else:
logger.info(f"{item_type}: {mapping_name} Details Update Not Needed")
logger.error(f"{item_type}: {mapping_name} Advanced Details Update Failed")
genres = []
if tmdb_item:
@ -505,9 +510,11 @@ class PlexAPI:
logger.error("Metadata Error: genre_sync_mode attribute must be either 'append' or 'sync' defaulting to append")
elif str(meta["genre_sync_mode"]).lower() == "sync":
for genre in (g for g in item_genres if g not in genres):
genre_updated = True
item.removeGenre(genre)
logger.info(f"Detail: Genre {genre} removed")
for genre in (g for g in genres if g not in item_genres):
genre_updated = True
item.addGenre(genre)
logger.info(f"Detail: Genre {genre} added")
@ -522,9 +529,11 @@ class PlexAPI:
logger.error("Metadata Error: label_sync_mode attribute must be either 'append' or 'sync' defaulting to append")
elif str(meta[methods["label_sync_mode"]]).lower() == "sync":
for label in (la for la in item_labels if la not in labels):
label_updated = True
item.removeLabel(label)
logger.info(f"Detail: Label {label} removed")
for label in (la for la in labels if la not in item_labels):
label_updated = True
item.addLabel(label)
logger.info(f"Detail: Label {label} added")
else:
@ -561,6 +570,7 @@ class PlexAPI:
add_edit("summary", season.summary, season_methods, season_dict)
if len(edits) > 0:
logger.debug(f"Season: {season_id} Details Update: {edits}")
season_updated = True
try:
season.edit(**edits)
season.reload()
@ -568,8 +578,6 @@ class PlexAPI:
except BadRequest:
util.print_stacktrace()
logger.error(f"Season: {season_id} Details Update Failed")
else:
logger.info(f"Season: {season_id} Details Update Not Needed")
else:
logger.error(f"Metadata Error: Season: {season_id} invalid, it must be an integer")
else:
@ -612,6 +620,7 @@ class PlexAPI:
add_edit("summary", episode.summary, episode_dict, episode_methods)
if len(edits) > 0:
logger.debug(f"Season: {season_id} Episode: {episode_id} Details Update: {edits}")
episode_updated = True
try:
episode.edit(**edits)
episode.reload()
@ -620,9 +629,10 @@ class PlexAPI:
except BadRequest:
util.print_stacktrace()
logger.error(f"Season: {season_id} Episode: {episode_id} Details Update Failed")
else:
logger.info(f"Season: {season_id} Episode: {episode_id} Details Update Not Needed")
else:
logger.error(f"Metadata Error: episode {episode_str} invalid must have S##E## format")
else:
logger.error("Metadata Error: episodes attribute is blank")
if not details_updated and not advance_details_updated and not genre_updated and not label_updated and not season_updated and not episode_updated:
logger.info(f"{item_type}: {mapping_name} Details Update Not Needed")

@ -111,6 +111,11 @@ class TraktAPI:
def send_request(self, url):
return requests.get(url, headers={"Content-Type": "application/json", "trakt-api-version": "2", "trakt-api-key": self.client_id}).json()
def get_collection(self, username, is_movie):
items = self.send_request(f"{self.base_url}/users/{username}/collection/{'movies' if is_movie else 'shows'}")
if is_movie: return [item["movie"]["ids"]["tmdb"] for item in items], []
else: return [], [item["show"]["ids"]["tvdb"] for item in items]
def get_pagenation(self, pagenation, amount, is_movie):
items = self.send_request(f"{self.base_url}/{'movies' if is_movie else 'shows'}/{pagenation}?limit={amount}")
if pagenation == "popular" and is_movie: return [item["ids"]["tmdb"] for item in items], []
@ -118,28 +123,26 @@ class TraktAPI:
elif is_movie: return [item["movie"]["ids"]["tmdb"] for item in items], []
else: return [], [item["show"]["ids"]["tvdb"] for item in items]
def validate_trakt_list(self, values):
trakt_values = []
for value in values:
try:
self.standard_list(value)
trakt_values.append(value)
except Failed as e:
logger.error(e)
if len(trakt_values) == 0:
raise Failed(f"Trakt Error: No valid Trakt Lists in {values}")
return trakt_values
def validate_trakt_watchlist(self, values, is_movie):
def validate_trakt(self, values, trakt_type=None, is_movie=None):
trakt_values = []
for value in values:
try:
if trakt_type == "watchlist" and is_movie is not None:
self.watchlist(value, is_movie)
elif trakt_type == "collection" and is_movie is not None:
self.get_collection(value, is_movie)
else:
self.standard_list(value)
trakt_values.append(value)
except Failed as e:
logger.error(e)
if len(trakt_values) == 0:
if trakt_type == "watchlist" and is_movie is not None:
raise Failed(f"Trakt Error: No valid Trakt Watchlists in {values}")
elif trakt_type == "collection" and is_movie is not None:
raise Failed(f"Trakt Error: No valid Trakt Collections in {values}")
else:
raise Failed(f"Trakt Error: No valid Trakt Lists in {values}")
return trakt_values
def get_items(self, method, data, is_movie, status_message=True):
@ -151,6 +154,10 @@ class TraktAPI:
movie_ids, show_ids = self.get_pagenation(method[6:], data, is_movie)
if status_message:
logger.info(f"Processing {pretty}: {data} {media_type}{'' if data == 1 else 's'}")
elif method == "trakt_collection":
movie_ids, show_ids = self.get_collection(data, is_movie)
if status_message:
logger.info(f"Processing {pretty} {media_type}s for {data}")
else:
show_ids = []
movie_ids = []

@ -155,6 +155,7 @@ pretty_names = {
"tmdb_writer": "TMDb Writer",
"tmdb_writer_details": "TMDb Writer",
"trakt_collected": "Trakt Collected",
"trakt_collection": "Trakt Collection",
"trakt_list": "Trakt List",
"trakt_list_details": "Trakt List",
"trakt_popular": "Trakt Popular",
@ -296,6 +297,7 @@ all_lists = [
"tmdb_writer",
"tmdb_writer_details",
"trakt_collected",
"trakt_collection",
"trakt_list",
"trakt_list_details",
"trakt_popular",

@ -72,7 +72,7 @@ file_handler.setFormatter(logging.Formatter("[%(asctime)s] %(filename)-27s %(lev
cmd_handler = logging.StreamHandler()
cmd_handler.setFormatter(logging.Formatter("| %(message)-100s |"))
cmd_handler.setLevel(logging.DEBUG if tests or test or debug else logging.INFO)
cmd_handler.setLevel(logging.DEBUG if my_tests or test or debug else logging.INFO)
logger.addHandler(cmd_handler)
logger.addHandler(file_handler)
@ -87,23 +87,23 @@ util.centered("| |_) | |/ _ \\ \\/ / | |\\/| |/ _ \\ __/ _` | | |\\/| |/ _` | '_
util.centered("| __/| | __/> < | | | | __/ || (_| | | | | | (_| | | | | (_| | (_| | __/ | ")
util.centered("|_| |_|\\___/_/\\_\\ |_| |_|\\___|\\__\\__,_| |_| |_|\\__,_|_| |_|\\__,_|\\__, |\\___|_| ")
util.centered(" |___/ ")
util.centered(" Version: 1.6.2 ")
util.centered(" Version: 1.6.4 ")
util.separator()
if my_tests:
tests.run_tests(default_dir)
sys.exit(0)
def start(config_path, is_test, daily, collections):
def start(config_path, is_test, daily, collections_to_run):
if daily: start_type = "Daily "
elif is_test: start_type = "Test "
elif collections: start_type = "Collections "
elif collections_to_run: start_type = "Collections "
else: start_type = ""
start_time = datetime.now()
util.separator(f"Starting {start_type}Run")
try:
config = Config(default_dir, config_path)
config.update_libraries(is_test, collections)
config.update_libraries(is_test, collections_to_run)
except Exception as e:
util.print_stacktrace()
logger.critical(e)

Loading…
Cancel
Save