mirror of
https://github.com/meisnate12/Plex-Meta-Manager
synced 2024-11-10 06:54:21 +00:00
re org
This commit is contained in:
parent
af725b8672
commit
26779c566d
9 changed files with 342 additions and 295 deletions
|
@ -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 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
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -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"}
|
||||
mass_update_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb"}
|
||||
|
||||
class Config:
|
||||
class ConfigFile:
|
||||
def __init__(self, default_dir, attrs):
|
||||
logger.info("Locating config...")
|
||||
config_file = attrs["config_file"]
|
||||
|
|
|
@ -37,12 +37,31 @@ class IMDb:
|
|||
if not isinstance(imdb_dict, dict):
|
||||
imdb_dict = {"url": 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()])):
|
||||
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}")
|
||||
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})
|
||||
return valid_lists
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import logging, os, requests, shutil, time
|
||||
from abc import ABC, abstractmethod
|
||||
from modules import util
|
||||
from modules.meta import Metadata
|
||||
from modules.meta import MetadataFile
|
||||
from modules.util import Failed, ImageData
|
||||
from PIL import Image
|
||||
from ruamel import yaml
|
||||
|
@ -92,7 +92,7 @@ class Library(ABC):
|
|||
metadata.append((file_type, metadata_file))
|
||||
for file_type, metadata_file in metadata:
|
||||
try:
|
||||
meta_obj = Metadata(config, self, file_type, metadata_file)
|
||||
meta_obj = MetadataFile(config, self, file_type, metadata_file)
|
||||
if meta_obj.collections:
|
||||
self.collections.extend([c for c in meta_obj.collections])
|
||||
if meta_obj.metadata:
|
||||
|
|
|
@ -9,7 +9,7 @@ logger = logging.getLogger("Plex Meta Manager")
|
|||
|
||||
github_base = "https://raw.githubusercontent.com/meisnate12/Plex-Meta-Manager-Configs/master/"
|
||||
|
||||
class Metadata:
|
||||
class MetadataFile:
|
||||
def __init__(self, config, library, file_type, path):
|
||||
self.config = config
|
||||
self.library = library
|
||||
|
@ -22,11 +22,15 @@ class Metadata:
|
|||
if attr_data[attribute]:
|
||||
if isinstance(attr_data[attribute], dict):
|
||||
new_dict = {}
|
||||
for a_name, a_data in attr_data[attribute].items():
|
||||
if a_name in check_list:
|
||||
logger.error(f"Config Warning: Skipping duplicate {attribute[:-1] if attribute[-1] == 's' else attribute}: {a_name}")
|
||||
for _name, _data in attr_data[attribute].items():
|
||||
if _name in check_list:
|
||||
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:
|
||||
new_dict[str(a_name)] = a_data
|
||||
new_dict[str(_name)] = _data
|
||||
return new_dict
|
||||
else:
|
||||
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")
|
||||
current = current[:-9]
|
||||
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:
|
||||
final_value = value
|
||||
if current != str(final_value):
|
||||
|
@ -174,7 +188,17 @@ class Metadata:
|
|||
logger.info("")
|
||||
year = None
|
||||
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
|
||||
if "title" in methods:
|
||||
|
|
|
@ -383,7 +383,7 @@ class Plex(Library):
|
|||
return choices
|
||||
except NotFound:
|
||||
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)
|
||||
def get_labels(self):
|
||||
|
|
|
@ -279,7 +279,7 @@ def time_window(time_window):
|
|||
|
||||
def glob_filter(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):
|
||||
if value is None:
|
||||
|
@ -326,72 +326,3 @@ def is_string_filter(values, modifier, data):
|
|||
if jailbreak: break
|
||||
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
|
||||
|
|
|
@ -5,8 +5,8 @@ try:
|
|||
import plexapi, schedule
|
||||
from modules import util
|
||||
from modules.builder import CollectionBuilder
|
||||
from modules.config import Config
|
||||
from modules.meta import Metadata
|
||||
from modules.config import ConfigFile
|
||||
from modules.meta import MetadataFile
|
||||
from modules.util import Failed, NotScheduled
|
||||
except ModuleNotFoundError:
|
||||
print("Requirements Error: Requirements are not installed")
|
||||
|
@ -159,7 +159,7 @@ def start(attrs):
|
|||
global stats
|
||||
stats = {"created": 0, "modified": 0, "deleted": 0, "added": 0, "removed": 0, "radarr": 0, "sonarr": 0}
|
||||
try:
|
||||
config = Config(default_dir, attrs)
|
||||
config = ConfigFile(default_dir, attrs)
|
||||
except Exception as e:
|
||||
util.print_stacktrace()
|
||||
util.print_multiline(e, critical=True)
|
||||
|
@ -535,7 +535,7 @@ def library_operations(config, library):
|
|||
logger.info("")
|
||||
util.separator(f"Starting TMDb Collections")
|
||||
logger.info("")
|
||||
metadata = Metadata(config, library, "Data", {
|
||||
metadata = MetadataFile(config, library, "Data", {
|
||||
"collections": {
|
||||
_n.replace(library.tmdb_collections["remove_suffix"], "").strip() if library.tmdb_collections["remove_suffix"] else _n:
|
||||
{"template": {"name": "TMDb Collection", "collection_id": _i}}
|
||||
|
|
Loading…
Reference in a new issue