Skip to content

Commit

Permalink
Merge pull request #264 from meisnate12/develop
Browse files Browse the repository at this point in the history
v1.9.2
  • Loading branch information
meisnate12 committed May 20, 2021
2 parents 69d6fbc + b8755bd commit 2770d17
Show file tree
Hide file tree
Showing 9 changed files with 334 additions and 180 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Plex Meta Manager
#### Version 1.9.1
#### Version 1.9.2

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.

Expand All @@ -19,7 +19,7 @@ The script is designed to work with most Metadata agents including the new Plex

## Support

* Before posting on Github about an enhancement, error, or configuration question please visit the [Plex Meta Manager Discord Server](https://discord.gg/NfH6mGFuAB).
* Before posting on Github about an enhancement, error, or configuration question please visit the [Plex Meta Manager Discord Server](https://discord.gg/TsdpsFYqqm).
* If you're getting an Error or have an Enhancement post in the [Issues](https://github.com/meisnate12/Plex-Meta-Manager/issues).
* If you have a configuration question post in the [Discussions](https://github.com/meisnate12/Plex-Meta-Manager/discussions).
* To see user submitted Metadata configuration files, and you to even add your own, go to the [Plex Meta Manager Configs](https://github.com/meisnate12/Plex-Meta-Manager-Configs).
Expand Down
137 changes: 77 additions & 60 deletions modules/builder.py

Large diffs are not rendered by default.

11 changes: 11 additions & 0 deletions modules/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -325,6 +325,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No
else:
params["name"] = str(library_name)
logger.info(f"Connecting to {params['name']} Library...")
params["mapping_name"] = str(library_name)

params["asset_directory"] = check_for_attribute(lib, "asset_directory", parent="settings", var_type="list_path", default=self.general["asset_directory"], default_is_none=True, save=False)
if params["asset_directory"] is None:
Expand Down Expand Up @@ -389,6 +390,16 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, default=No
else:
params["mass_critic_rating_update"] = None

if lib and "radarr_add_all" in lib and lib["radarr_add_all"]:
params["radarr_add_all"] = check_for_attribute(lib, "radarr_add_all", var_type="bool", default=False, save=False)
else:
params["radarr_add_all"] = None

if lib and "sonarr_add_all" in lib and lib["sonarr_add_all"]:
params["sonarr_add_all"] = check_for_attribute(lib, "sonarr_add_all", var_type="bool", default=False, save=False)
else:
params["sonarr_add_all"] = None

try:
if lib and "metadata_path" in lib:
params["metadata_path"] = []
Expand Down
9 changes: 7 additions & 2 deletions modules/convert.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from lxml import html
from modules import util
from modules.util import Failed
from plexapi.exceptions import BadRequest
from retrying import retry

logger = logging.getLogger("Plex Meta Manager")
Expand Down Expand Up @@ -276,6 +277,8 @@ def get_id(self, item, library, length):
except requests.exceptions.ConnectionError:
util.print_stacktrace()
raise Failed("No External GUIDs found")
if not tvdb_id and not imdb_id and not tmdb_id:
raise Failed("Refresh Metadata")
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)
Expand Down Expand Up @@ -354,7 +357,9 @@ def update_cache(cache_ids, id_type, guid_type):
return "movie", tmdb_id
else:
raise Failed(f"No ID to convert")

except Failed as e:
util.print_end(length, f"Mapping Error | {item.guid:<46} | {e} for {item.title}")
return None, None
except BadRequest:
util.print_stacktrace()
util.print_end(length, f"Mapping Error: | {item.guid} for {item.title} not found")
return None, None
50 changes: 32 additions & 18 deletions modules/meta.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,30 +121,44 @@ def add_advanced_edit(attr, obj, group, alias, show_library=False, new_agent=Fal
def edit_tags(attr, obj, group, alias, key=None, extra=None, movie_library=False):
if key is None:
key = f"{attr}s"
if attr in alias and f"{attr}.sync" in alias:
if movie_library and not self.library.is_movie:
logger.error(f"Metadata Error: {attr} attribute only works for movie libraries")
elif attr in alias and f"{attr}.sync" in alias:
logger.error(f"Metadata Error: Cannot use {attr} and {attr}.sync together")
elif attr in alias or f"{attr}.sync" in alias:
elif f"{attr}.remove" in alias and f"{attr}.sync" in alias:
logger.error(f"Metadata Error: Cannot use {attr}.remove and {attr}.sync together")
elif attr in alias and group[alias[attr]] is None:
logger.error(f"Metadata Error: {attr} attribute is blank")
elif f"{attr}.remove" in alias and group[alias[f"{attr}.remove"]] is None:
logger.error(f"Metadata Error: {attr}.remove attribute is blank")
elif f"{attr}.sync" in alias and group[alias[f"{attr}.sync"]] is None:
logger.error(f"Metadata Error: {attr}.sync attribute is blank")
elif attr in alias or f"{attr}.remove" in alias or f"{attr}.sync" in alias:
attr_key = attr if attr in alias else f"{attr}.sync"
if movie_library and not self.library.is_movie:
logger.error(f"Metadata Error: {attr_key} attribute only works for movie libraries")
elif group[alias[attr_key]] or extra:
item_tags = [item_tag.tag for item_tag in getattr(obj, key)]
input_tags = []
if group[alias[attr_key]]:
input_tags.extend(util.get_list(group[alias[attr_key]]))
if extra:
input_tags.extend(extra)
if f"{attr}.sync" in alias:
remove_method = getattr(obj, f"remove{attr.capitalize()}")
for tag in (t for t in item_tags if t not in input_tags):
updated = True
remove_method(tag)
logger.info(f"Detail: {attr.capitalize()} {tag} removed")
item_tags = [item_tag.tag for item_tag in getattr(obj, key)]
input_tags = []
if group[alias[attr_key]]:
input_tags.extend(util.get_list(group[alias[attr_key]]))
if extra:
input_tags.extend(extra)
if f"{attr}.sync" in alias:
remove_method = getattr(obj, f"remove{attr.capitalize()}")
for tag in (t for t in item_tags if t not in input_tags):
updated = True
self.library.query_data(remove_method, tag)
logger.info(f"Detail: {attr.capitalize()} {tag} removed")
if attr in alias or f"{attr}.sync" in alias:
add_method = getattr(obj, f"add{attr.capitalize()}")
for tag in (t for t in input_tags if t not in item_tags):
updated = True
add_method(tag)
self.library.query_data(add_method, tag)
logger.info(f"Detail: {attr.capitalize()} {tag} added")
if f"{attr}.remove" in alias:
remove_method = getattr(obj, f"remove{attr.capitalize()}")
for tag in util.get_list(group[alias[f"{attr}.remove"]]):
if tag in item_tags:
self.library.query_data(remove_method, tag)
logger.info(f"Detail: {attr.capitalize()} {tag} removed")
else:
logger.error(f"Metadata Error: {attr} attribute is blank")

Expand Down
60 changes: 44 additions & 16 deletions modules/plex.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from modules import util
from modules.meta import Metadata
from modules.util import Failed
import plexapi
from plexapi import utils
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
from plexapi.collection import Collections
Expand Down Expand Up @@ -182,7 +183,7 @@
"producer", "producer.not",
"subtitle_language", "subtitle_language.not",
"writer", "writer.not",
"decade", "resolution",
"decade", "resolution", "hdr",
"added", "added.not", "added.before", "added.after",
"originally_available", "originally_available.not",
"originally_available.before", "originally_available.after",
Expand Down Expand Up @@ -323,6 +324,7 @@ def __init__(self, params, TMDb, TVDb):
self.Sonarr = None
self.Tautulli = None
self.name = params["name"]
self.mapping_name = util.validate_filename(params["mapping_name"])
self.missing_path = os.path.join(params["default_dir"], f"{self.name}_missing.yml")
self.metadata_path = params["metadata_path"]
self.asset_directory = params["asset_directory"]
Expand All @@ -336,7 +338,9 @@ def __init__(self, params, TMDb, TVDb):
self.mass_genre_update = params["mass_genre_update"]
self.mass_audience_rating_update = params["mass_audience_rating_update"]
self.mass_critic_rating_update = params["mass_critic_rating_update"]
self.mass_update = self.mass_genre_update or self.mass_audience_rating_update or self.mass_critic_rating_update
self.radarr_add_all = params["radarr_add_all"]
self.sonarr_add_all = params["sonarr_add_all"]
self.mass_update = self.mass_genre_update or self.mass_audience_rating_update or self.mass_critic_rating_update or self.radarr_add_all or self.sonarr_add_all
self.plex = params["plex"]
self.url = params["plex"]["url"]
self.token = params["plex"]["token"]
Expand Down Expand Up @@ -396,6 +400,11 @@ def collection_order_query(self, collection, data):

@retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex)
def get_guids(self, item):
item.reload(checkFiles=False, includeAllConcerts=False, includeBandwidths=False, includeChapters=False,
includeChildren=False, includeConcerts=False, includeExternalMedia=False, inclueExtras=False,
includeFields='', includeGeolocation=False, includeLoudnessRamps=False, includeMarkers=False,
includeOnDeck=False, includePopularLeaves=False, includePreferences=False, includeRelated=False,
includeRelatedCount=0, includeReviews=False, includeStations=False)
return item.guids

@retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex)
Expand Down Expand Up @@ -560,16 +569,16 @@ def get_items(self, method, data):
or_des = conjunction if o > 0 else f"{search_method}("
ors += f"{or_des}{param}"
if has_processed:
logger.info(f"\t\t AND {ors})")
logger.info(f" AND {ors})")
else:
logger.info(f"Processing {pretty}: {ors})")
has_processed = True
if search_sort:
logger.info(f"\t\t SORT BY {search_sort})")
logger.info(f" SORT BY {search_sort}")
if search_limit:
logger.info(f"\t\t LIMIT {search_limit})")
logger.info(f" LIMIT {search_limit}")
logger.debug(f"Search: {search_terms}")
return self.search(sort=sorts[search_sort], maxresults=search_limit, **search_terms)
items = self.search(sort=sorts[search_sort], maxresults=search_limit, **search_terms)
elif method == "plex_collectionless":
good_collections = []
logger.info("Collections Excluded")
Expand Down Expand Up @@ -610,7 +619,7 @@ def get_items(self, method, data):
else:
raise Failed(f"Plex Error: Method {method} not supported")
if len(items) > 0:
return items
return [item.ratingKey for item in items]
else:
raise Failed("Plex Error: No Items found in Plex")

Expand All @@ -633,7 +642,11 @@ def get_collection_items(self, collection, smart_label_collection):
if smart_label_collection:
return self.get_labeled_items(collection.title if isinstance(collection, Collections) else str(collection))
elif isinstance(collection, Collections):
return self.query(collection.items)
if self.smart(collection):
key = f"/library/sections/{self.Plex.key}/all{self.smart_filter(collection)}"
return self.Plex._search(key, None, 0, plexapi.X_PLEX_CONTAINER_SIZE)
else:
return self.query(collection.items)
else:
return []

Expand All @@ -659,11 +672,16 @@ def edit_item(self, item, name, item_type, edits, advanced=False):
util.print_stacktrace()
logger.error(f"{item_type}: {name}{' Advanced' if advanced else ''} Details Update Failed")

def update_item_from_assets(self, item, dirs=None):
def update_item_from_assets(self, item, collection_mode=False, upload=True, dirs=None, name=None):
if dirs is None:
dirs = self.asset_directory
name = os.path.basename(os.path.dirname(item.locations[0]) if self.is_movie else item.locations[0])
if not name and collection_mode:
name = item.title
elif not name:
name = os.path.basename(os.path.dirname(item.locations[0]) if self.is_movie else item.locations[0])
for ad in dirs:
poster_image = None
background_image = None
if self.asset_folders:
if not os.path.isdir(os.path.join(ad, name)):
continue
Expand All @@ -674,13 +692,22 @@ def update_item_from_assets(self, item, dirs=None):
background_filter = os.path.join(ad, f"{name}_background.*")
matches = glob.glob(poster_filter)
if len(matches) > 0:
self.upload_image(item, os.path.abspath(matches[0]), url=False)
logger.info(f"Detail: asset_directory updated {item.title}'s poster to [file] {os.path.abspath(matches[0])}")
poster_image = os.path.abspath(matches[0])
if upload:
self.upload_image(item, poster_image, url=False)
logger.info(f"Detail: asset_directory updated {item.title}'s poster to [file] {poster_image}")
matches = glob.glob(background_filter)
if len(matches) > 0:
self.upload_image(item, os.path.abspath(matches[0]), poster=False, url=False)
logger.info(f"Detail: asset_directory updated {item.title}'s background to [file] {os.path.abspath(matches[0])}")
if self.is_show:
background_image = os.path.abspath(matches[0])
if upload:
self.upload_image(item, background_image, poster=False, url=False)
logger.info(f"Detail: asset_directory updated {item.title}'s background to [file] {background_image}")
if collection_mode:
for ite in self.query(item.items):
self.update_item_from_assets(ite, dirs=[os.path.join(ad, name)])
if not upload:
return poster_image, background_image
if self.is_show and not collection_mode:
for season in self.query(item.seasons):
if self.asset_folders:
season_filter = os.path.join(ad, name, f"Season{'0' if season.seasonNumber < 10 else ''}{season.seasonNumber}.*")
Expand All @@ -700,4 +727,5 @@ def update_item_from_assets(self, item, dirs=None):
if len(matches) > 0:
episode_path = os.path.abspath(matches[0])
self.upload_image(episode, episode_path, url=False)
logger.info(f"Detail: asset_directory updated {item.title} {episode.seasonEpisode.upper()}'s poster to [file] {episode_path}")
logger.info(f"Detail: asset_directory updated {item.title} {episode.seasonEpisode.upper()}'s poster to [file] {episode_path}")
return None, None
24 changes: 19 additions & 5 deletions modules/util.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import logging, re, signal, sys, time, traceback
from datetime import datetime
from pathvalidate import is_valid_filename, sanitize_filename
from plexapi.exceptions import BadRequest, NotFound, Unauthorized

try:
Expand Down Expand Up @@ -220,7 +221,6 @@ def compile_list(data):
else:
return data


def get_list(data, lower=False, split=True, int_list=False):
if isinstance(data, list): return data
elif isinstance(data, dict): return [data]
Expand Down Expand Up @@ -366,16 +366,22 @@ def centered(text, do_print=True):
return final_text

def separator(text=None):
logger.handlers[0].setFormatter(logging.Formatter(f"%(message)-{screen_width - 2}s"))
logger.handlers[1].setFormatter(logging.Formatter(f"[%(asctime)s] %(filename)-27s %(levelname)-10s %(message)-{screen_width - 2}s"))
for handler in logger.handlers:
apply_formatter(handler, border=False)
logger.info(f"|{separating_character * screen_width}|")
if text:
text_list = text.split("\n")
for t in text_list:
logger.info(f"| {centered(t, do_print=False)} |")
logger.info(f"|{separating_character * screen_width}|")
logger.handlers[0].setFormatter(logging.Formatter(f"| %(message)-{screen_width - 2}s |"))
logger.handlers[1].setFormatter(logging.Formatter(f"[%(asctime)s] %(filename)-27s %(levelname)-10s | %(message)-{screen_width - 2}s |"))
for handler in logger.handlers:
apply_formatter(handler)

def apply_formatter(handler, border=True):
text = f"| %(message)-{screen_width - 2}s |" if border else f"%(message)-{screen_width - 2}s"
if isinstance(handler, logging.handlers.RotatingFileHandler):
text = f"[%(asctime)s] %(filename)-27s %(levelname)-10s {text}"
handler.setFormatter(logging.Formatter(text))

def print_return(length, text):
print(adjust_space(length, f"| {text}"), end="\r")
Expand All @@ -384,3 +390,11 @@ def print_return(length, text):
def print_end(length, text=None):
if text: logger.info(adjust_space(length, text))
else: print(adjust_space(length, " "), end="\r")

def validate_filename(filename):
if is_valid_filename(filename):
return filename
else:
mapping_name = sanitize_filename(filename)
logger.info(f"Folder Name: {filename} is invalid using {mapping_name}")
return mapping_name

0 comments on commit 2770d17

Please sign in to comment.