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 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
|
@ -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"]
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -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
|
|
||||||
|
|
|
@ -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}}
|
||||||
|
|
Loading…
Reference in a new issue