This commit is contained in:
meisnate12 2021-12-13 02:30:19 -05:00
parent af725b8672
commit 26779c566d
9 changed files with 342 additions and 295 deletions

View file

@ -12,7 +12,7 @@ The original concept for Plex Meta Manager is [Plex Auto Collections](https://gi
The script can update many metadata fields for movies, shows, collections, seasons, and episodes and can act as a backup if your plex DB goes down. It can even update metadata the plex UI can't like Season Names. If the time is put into the metadata configuration file you can have a way to recreate your library and all its metadata changes with the click of a button. The script can update many metadata fields for movies, shows, collections, seasons, and episodes and can act as a backup if your plex DB goes down. It can even update metadata the plex UI can't like Season Names. If the time is put into the metadata configuration file you can have a way to recreate your library and all its metadata changes with the click of a button.
The script works 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). The script works with most Metadata agents including the New Plex Movie Agent, New Plex TV Agent, [Hama Anime Agent](https://github.com/ZeroQI/Hama.bundle), [MyAnimeList Anime Agent](https://github.com/Fribb/MyAnimeList.bundle), and [XBMC NFO Movie and TV Agents](https://github.com/gboudreau/XBMCnfoMoviesImporter.bundle).
## Getting Started ## Getting Started

File diff suppressed because it is too large Load diff

View file

@ -31,7 +31,7 @@ logger = logging.getLogger("Plex Meta Manager")
sync_modes = {"append": "Only Add Items to the Collection", "sync": "Add & Remove Items from the Collection"} sync_modes = {"append": "Only Add Items to the Collection", "sync": "Add & Remove Items from the Collection"}
mass_update_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb"} mass_update_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb"}
class Config: class ConfigFile:
def __init__(self, default_dir, attrs): def __init__(self, default_dir, attrs):
logger.info("Locating config...") logger.info("Locating config...")
config_file = attrs["config_file"] config_file = attrs["config_file"]

View file

@ -37,12 +37,31 @@ class IMDb:
if not isinstance(imdb_dict, dict): if not isinstance(imdb_dict, dict):
imdb_dict = {"url": imdb_dict} imdb_dict = {"url": imdb_dict}
dict_methods = {dm.lower(): dm for dm in imdb_dict} dict_methods = {dm.lower(): dm for dm in imdb_dict}
imdb_url = util.parse("url", imdb_dict, methods=dict_methods, parent="imdb_list").strip() if "url" not in dict_methods:
raise Failed(f"Collection Error: imdb_list url attribute not found")
elif imdb_dict[dict_methods["url"]] is None:
raise Failed(f"Collection Error: imdb_list url attribute is blank")
else:
imdb_url = imdb_dict[dict_methods["url"]].strip()
if not imdb_url.startswith(tuple([v for k, v in urls.items()])): if not imdb_url.startswith(tuple([v for k, v in urls.items()])):
fails = "\n".join([f"{v} (For {k.replace('_', ' ').title()})" for k, v in urls.items()]) fails = "\n".join([f"{v} (For {k.replace('_', ' ').title()})" for k, v in urls.items()])
raise Failed(f"IMDb Error: {imdb_url} must begin with either:{fails}") raise Failed(f"IMDb Error: {imdb_url} must begin with either:{fails}")
self._total(imdb_url, language) self._total(imdb_url, language)
list_count = util.parse("limit", imdb_dict, datatype="int", methods=dict_methods, default=0, parent="imdb_list", minimum=0) if "limit" in dict_methods else 0 list_count = None
if "limit" in dict_methods:
if imdb_dict[dict_methods["limit"]] is None:
logger.warning(f"Collection Warning: imdb_list limit attribute is blank using 0 as default")
else:
try:
value = int(str(imdb_dict[dict_methods["limit"]]))
if 0 <= value:
list_count = value
except ValueError:
pass
if list_count is None:
logger.warning(f"Collection Warning: imdb_list limit attribute must be an integer 0 or greater using 0 as default")
if list_count is None:
list_count = 0
valid_lists.append({"url": imdb_url, "limit": list_count}) valid_lists.append({"url": imdb_url, "limit": list_count})
return valid_lists return valid_lists

View file

@ -1,7 +1,7 @@
import logging, os, requests, shutil, time import logging, os, requests, shutil, time
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from modules import util from modules import util
from modules.meta import Metadata from modules.meta import MetadataFile
from modules.util import Failed, ImageData from modules.util import Failed, ImageData
from PIL import Image from PIL import Image
from ruamel import yaml from ruamel import yaml
@ -92,7 +92,7 @@ class Library(ABC):
metadata.append((file_type, metadata_file)) metadata.append((file_type, metadata_file))
for file_type, metadata_file in metadata: for file_type, metadata_file in metadata:
try: try:
meta_obj = Metadata(config, self, file_type, metadata_file) meta_obj = MetadataFile(config, self, file_type, metadata_file)
if meta_obj.collections: if meta_obj.collections:
self.collections.extend([c for c in meta_obj.collections]) self.collections.extend([c for c in meta_obj.collections])
if meta_obj.metadata: if meta_obj.metadata:

View file

@ -9,7 +9,7 @@ logger = logging.getLogger("Plex Meta Manager")
github_base = "https://raw.githubusercontent.com/meisnate12/Plex-Meta-Manager-Configs/master/" github_base = "https://raw.githubusercontent.com/meisnate12/Plex-Meta-Manager-Configs/master/"
class Metadata: class MetadataFile:
def __init__(self, config, library, file_type, path): def __init__(self, config, library, file_type, path):
self.config = config self.config = config
self.library = library self.library = library
@ -22,11 +22,15 @@ class Metadata:
if attr_data[attribute]: if attr_data[attribute]:
if isinstance(attr_data[attribute], dict): if isinstance(attr_data[attribute], dict):
new_dict = {} new_dict = {}
for a_name, a_data in attr_data[attribute].items(): for _name, _data in attr_data[attribute].items():
if a_name in check_list: if _name in check_list:
logger.error(f"Config Warning: Skipping duplicate {attribute[:-1] if attribute[-1] == 's' else attribute}: {a_name}") logger.error(f"Config Warning: Skipping duplicate {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name}")
elif _data is None:
logger.error(f"Config Warning: {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name} has no data")
elif not isinstance(_data, dict):
logger.error(f"Config Warning: {attribute[:-1] if attribute[-1] == 's' else attribute}: {_name} must be a dictionary")
else: else:
new_dict[str(a_name)] = a_data new_dict[str(_name)] = _data
return new_dict return new_dict
else: else:
logger.warning(f"Config Warning: {attribute} must be a dictionary") logger.warning(f"Config Warning: {attribute} must be a dictionary")
@ -97,7 +101,17 @@ class Metadata:
final_value = util.validate_date(value, name, return_as="%Y-%m-%d") final_value = util.validate_date(value, name, return_as="%Y-%m-%d")
current = current[:-9] current = current[:-9]
elif var_type == "float": elif var_type == "float":
final_value = util.parse(name, value, datatype="float", minimum=0, maximum=10) if value is None:
raise Failed(f"Metadata Error: {name} attribute is blank")
final_value = None
try:
value = float(str(value))
if 0 <= value <= 10:
final_value = value
except ValueError:
pass
if final_value is None:
raise Failed(f"Metadata Error: {name} attribute must be a number between 0 and 10")
else: else:
final_value = value final_value = value
if current != str(final_value): if current != str(final_value):
@ -174,7 +188,17 @@ class Metadata:
logger.info("") logger.info("")
year = None year = None
if "year" in methods: if "year" in methods:
year = util.parse("year", meta, datatype="int", methods=methods, minimum=1800, maximum=datetime.now().year + 1) next_year = datetime.now().year + 1
if meta[methods["year"]] is None:
raise Failed("Metadata Error: year attribute is blank")
try:
year_value = int(str(meta[methods["year"]]))
if 1800 <= year_value <= next_year:
year = year_value
except ValueError:
pass
if year is None:
raise Failed(f"Metadata Error: year attribute must be an integer between 1800 and {next_year}")
title = mapping_name title = mapping_name
if "title" in methods: if "title" in methods:

View file

@ -383,7 +383,7 @@ class Plex(Library):
return choices return choices
except NotFound: except NotFound:
logger.debug(f"Search Attribute: {final_search}") logger.debug(f"Search Attribute: {final_search}")
raise Failed(f"Collection Error: plex search attribute: {search_name} not supported") raise Failed(f"Plex Error: plex_search attribute: {search_name} not supported")
@retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex) @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_plex)
def get_labels(self): def get_labels(self):

View file

@ -279,7 +279,7 @@ def time_window(time_window):
def glob_filter(filter_in): def glob_filter(filter_in):
filter_in = filter_in.translate({ord("["): "[[]", ord("]"): "[]]"}) if "[" in filter_in else filter_in filter_in = filter_in.translate({ord("["): "[[]", ord("]"): "[]]"}) if "[" in filter_in else filter_in
return glob.glob(filter_in, recursive=True) return glob.glob(filter_in)
def is_date_filter(value, modifier, data, final, current_time): def is_date_filter(value, modifier, data, final, current_time):
if value is None: if value is None:
@ -326,72 +326,3 @@ def is_string_filter(values, modifier, data):
if jailbreak: break if jailbreak: break
return (jailbreak and modifier in [".not", ".isnot"]) or (not jailbreak and modifier in ["", ".is", ".begins", ".ends", ".regex"]) return (jailbreak and modifier in [".not", ".isnot"]) or (not jailbreak and modifier in ["", ".is", ".begins", ".ends", ".regex"])
def parse(attribute, data, datatype=None, methods=None, parent=None, default=None, options=None, translation=None, minimum=1, maximum=None, regex=None):
display = f"{parent + ' ' if parent else ''}{attribute} attribute"
if options is None and translation is not None:
options = [o for o in translation]
value = data[methods[attribute]] if methods and attribute in methods else data
if datatype == "list":
if value:
return [v for v in value if v] if isinstance(value, list) else [str(value)]
return []
elif datatype == "intlist":
if value:
try:
return [int(v) for v in value if v] if isinstance(value, list) else [int(value)]
except ValueError:
pass
return []
elif datatype == "dictlist":
final_list = []
for dict_data in get_list(value):
if isinstance(dict_data, dict):
final_list.append((dict_data, {dm.lower(): dm for dm in dict_data}))
else:
raise Failed(f"Collection Error: {display} {dict_data} is not a dictionary")
return final_list
elif methods and attribute not in methods:
message = f"{display} not found"
elif value is None:
message = f"{display} is blank"
elif regex is not None:
regex_str, example = regex
if re.compile(regex_str).match(str(value)):
return str(value)
else:
message = f"{display}: {value} must match pattern {regex_str} e.g. {example}"
elif datatype == "bool":
if isinstance(value, bool):
return value
elif isinstance(value, int):
return value > 0
elif str(value).lower() in ["t", "true"]:
return True
elif str(value).lower() in ["f", "false"]:
return False
else:
message = f"{display} must be either true or false"
elif datatype in ["int", "float"]:
try:
value = int(str(value)) if datatype == "int" else float(str(value))
if (maximum is None and minimum <= value) or (maximum is not None and minimum <= value <= maximum):
return value
except ValueError:
pass
pre = f"{display} {value} must {'an integer' if datatype == 'int' else 'a number'}"
if maximum is None:
message = f"{pre} {minimum} or greater"
else:
message = f"{pre} between {minimum} and {maximum}"
elif (translation is not None and str(value).lower() not in translation) or \
(options is not None and translation is None and str(value).lower() not in options):
message = f"{display} {value} must be in {', '.join([str(o) for o in options])}"
else:
return translation[value] if translation is not None else value
if default is None:
raise Failed(f"Collection Error: {message}")
else:
logger.warning(f"Collection Warning: {message} using {default} as default")
return translation[default] if translation is not None else default

View file

@ -5,8 +5,8 @@ try:
import plexapi, schedule import plexapi, schedule
from modules import util from modules import util
from modules.builder import CollectionBuilder from modules.builder import CollectionBuilder
from modules.config import Config from modules.config import ConfigFile
from modules.meta import Metadata from modules.meta import MetadataFile
from modules.util import Failed, NotScheduled from modules.util import Failed, NotScheduled
except ModuleNotFoundError: except ModuleNotFoundError:
print("Requirements Error: Requirements are not installed") print("Requirements Error: Requirements are not installed")
@ -159,7 +159,7 @@ def start(attrs):
global stats global stats
stats = {"created": 0, "modified": 0, "deleted": 0, "added": 0, "removed": 0, "radarr": 0, "sonarr": 0} stats = {"created": 0, "modified": 0, "deleted": 0, "added": 0, "removed": 0, "radarr": 0, "sonarr": 0}
try: try:
config = Config(default_dir, attrs) config = ConfigFile(default_dir, attrs)
except Exception as e: except Exception as e:
util.print_stacktrace() util.print_stacktrace()
util.print_multiline(e, critical=True) util.print_multiline(e, critical=True)
@ -535,7 +535,7 @@ def library_operations(config, library):
logger.info("") logger.info("")
util.separator(f"Starting TMDb Collections") util.separator(f"Starting TMDb Collections")
logger.info("") logger.info("")
metadata = Metadata(config, library, "Data", { metadata = MetadataFile(config, library, "Data", {
"collections": { "collections": {
_n.replace(library.tmdb_collections["remove_suffix"], "").strip() if library.tmdb_collections["remove_suffix"] else _n: _n.replace(library.tmdb_collections["remove_suffix"], "").strip() if library.tmdb_collections["remove_suffix"] else _n:
{"template": {"name": "TMDb Collection", "collection_id": _i}} {"template": {"name": "TMDb Collection", "collection_id": _i}}