You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
Plex-Meta-Manager/modules/config.py

1205 lines
102 KiB

import glob, json, logging, os, re, requests
from datetime import datetime, timedelta
from modules import util
from modules.anidb import AniDBAPI
from modules.cache import Cache
from modules.imdb import IMDbAPI
from modules.plex import PlexAPI
from modules.mal import MyAnimeListAPI
from modules.mal import MyAnimeListIDList
from modules.tmdb import TMDbAPI
from modules.trakt import TraktAPI
from modules.tvdb import TVDbAPI
from modules.util import Failed
from ruamel import yaml
logger = logging.getLogger("Plex Meta Manager")
class Config:
def __init__(self, default_dir, config_path=None):
logger.info("Locating config...")
if config_path and os.path.exists(config_path): self.config_path = os.path.abspath(config_path)
elif config_path and not os.path.exists(config_path): raise Failed("Config Error: config not found at {}".format(os.path.abspath(config_path)))
elif os.path.exists(os.path.join(default_dir, "config.yml")): self.config_path = os.path.abspath(os.path.join(default_dir, "config.yml"))
else: raise Failed("Config Error: config not found at {}".format(os.path.abspath(default_dir)))
logger.info("Using {} as config".format(self.config_path))
yaml.YAML().allow_duplicate_keys = True
try: self.data, ind, bsi = yaml.util.load_yaml_guess_indent(open(self.config_path))
except yaml.scanner.ScannerError as e: raise Failed("YAML Error: {}".format(str(e).replace("\n", "\n|\t ")))
def check_for_attribute(data, attribute, parent=None, test_list=None, options="", default=None, do_print=True, default_is_none=False, var_type="str", throw=False, save=True):
message = ""
endline = ""
data = data if parent is None else data[parent]
text = "{} attribute".format(attribute) if parent is None else "{} sub-attribute {}".format(parent, attribute)
if data is None or attribute not in data:
message = "Config Error: {} not found".format(text)
if parent and save is True:
new_config, ind, bsi = yaml.util.load_yaml_guess_indent(open(self.config_path))
endline = "\n| {} sub-attribute {} added to config".format(parent, attribute)
if parent not in new_config: new_config = {parent: {attribute: default}}
elif not new_config[parent]: new_config[parent] = {attribute: default}
elif attribute not in new_config[parent]: new_config[parent][attribute] = default
else: endLine = ""
yaml.round_trip_dump(new_config, open(self.config_path, "w"), indent=ind, block_seq_indent=bsi)
elif not data[attribute] and data[attribute] != False: message = "Config Error: {} is blank".format(text)
elif var_type == "bool":
if isinstance(data[attribute], bool): return data[attribute]
else: message = "Config Error: {} must be either true or false".format(text)
elif var_type == "int":
if isinstance(data[attribute], int) and data[attribute] > 0: return data[attribute]
else: message = "Config Error: {} must an integer > 0".format(text)
elif var_type == "path":
if os.path.exists(os.path.abspath(data[attribute])): return data[attribute]
else: message = "Config Error: {} could not be found".format(text)
if default and os.path.exists(os.path.abspath(default)):
return default
elif default:
default = None
default_is_none = True
message = "Config Error: neither {} or the default path {} could be found".format(data[attribute], default)
elif test_list is None or data[attribute] in test_list: return data[attribute]
else: message = "Config Error: {}: {} is an invalid input".format(text, data[attribute])
if default is not None or default_is_none:
message = message + " using {} as default".format(default)
message = message + endline
if (default is None and not default_is_none) or throw:
if len(options) > 0:
message = message + "\n" + options
raise Failed(message)
if do_print:
util.print_multiline(message)
if attribute in data and data[attribute] and test_list is not None and data[attribute] not in test_list:
util.print_multiline(options)
return default
self.general = {}
self.general["cache"] = check_for_attribute(self.data, "cache", parent="cache", options="| \ttrue (Create a cache to store ids)\n| \tfalse (Do not create a cache to store ids)", var_type="bool", default=True) if "cache" in self.data else True
self.general["cache_expiration"] = check_for_attribute(self.data, "cache_expiration", parent="cache", var_type="int", default=60) if "cache" in self.data else 60
if self.general["cache"]:
util.seperator()
self.Cache = Cache(self.config_path, self.general["cache_expiration"])
else:
self.Cache = None
util.seperator()
self.TMDb = None
if "tmdb" in self.data:
logger.info("Connecting to TMDb...")
self.tmdb = {}
self.tmdb["apikey"] = check_for_attribute(self.data, "apikey", parent="tmdb", throw=True)
self.tmdb["language"] = check_for_attribute(self.data, "language", parent="tmdb", default="en")
self.TMDb = TMDbAPI(self.tmdb)
logger.info("TMDb Connection {}".format("Failed" if self.TMDb is None else "Successful"))
else:
raise Failed("Config Error: tmdb attribute not found")
util.seperator()
self.Trakt = None
if "trakt" in self.data:
logger.info("Connecting to Trakt...")
self.trakt = {}
try:
self.trakt["client_id"] = check_for_attribute(self.data, "client_id", parent="trakt", throw=True)
self.trakt["client_secret"] = check_for_attribute(self.data, "client_secret", parent="trakt", throw=True)
self.trakt["config_path"] = self.config_path
authorization = self.data["trakt"]["authorization"] if "authorization" in self.data["trakt"] and self.data["trakt"]["authorization"] else None
self.Trakt = TraktAPI(self.trakt, authorization)
except Failed as e:
logger.error(e)
logger.info("Trakt Connection {}".format("Failed" if self.Trakt is None else "Successful"))
else:
logger.warning("trakt attribute not found")
util.seperator()
self.MyAnimeList = None
self.MyAnimeListIDList = MyAnimeListIDList()
if "mal" in self.data:
logger.info("Connecting to My Anime List...")
self.mal = {}
try:
self.mal["client_id"] = check_for_attribute(self.data, "client_id", parent="mal", throw=True)
self.mal["client_secret"] = check_for_attribute(self.data, "client_secret", parent="mal", throw=True)
self.mal["config_path"] = self.config_path
authorization = self.data["mal"]["authorization"] if "authorization" in self.data["mal"] and self.data["mal"]["authorization"] else None
self.MyAnimeList = MyAnimeListAPI(self.mal, self.MyAnimeListIDList, authorization)
except Failed as e:
logger.error(e)
logger.info("My Anime List Connection {}".format("Failed" if self.Trakt is None else "Successful"))
else:
logger.warning("mal attribute not found")
self.TVDb = TVDbAPI(Cache=self.Cache, TMDb=self.TMDb, Trakt=self.Trakt)
self.IMDb = IMDbAPI(Cache=self.Cache, TMDb=self.TMDb, Trakt=self.Trakt, TVDb=self.TVDb) if self.TMDb or self.Trakt else None
self.AniDB = AniDBAPI(Cache=self.Cache, TMDb=self.TMDb, Trakt=self.Trakt)
self.general["plex"] = {}
self.general["plex"]["url"] = check_for_attribute(self.data, "url", parent="plex", default_is_none=True) if "plex" in self.data else None
self.general["plex"]["token"] = check_for_attribute(self.data, "token", parent="plex", default_is_none=True) if "plex" in self.data else None
self.general["plex"]["asset_directory"] = check_for_attribute(self.data, "asset_directory", parent="plex", var_type="path", default=os.path.join(default_dir, "assets")) if "plex" in self.data else os.path.join(default_dir, "assets")
self.general["plex"]["sync_mode"] = check_for_attribute(self.data, "sync_mode", parent="plex", default="append", test_list=["append", "sync"], options="| \tappend (Only Add Items to the Collection)\n| \tsync (Add & Remove Items from the Collection)") if "plex" in self.data else "append"
self.general["plex"]["show_unmanaged_collections"] = check_for_attribute(self.data, "show_unmanaged_collections", parent="plex", var_type="bool", default=True) if "plex" in self.data else True
self.general["radarr"] = {}
self.general["radarr"]["url"] = check_for_attribute(self.data, "url", parent="radarr", default_is_none=True) if "radarr" in self.data else None
self.general["radarr"]["version"] = check_for_attribute(self.data, "version", parent="radarr", test_list=["v2", "v3"], options="| \tv2 (For Radarr 0.2)\n| \tv3 (For Radarr 3.0)", default="v2") if "radarr" in self.data else "v2"
self.general["radarr"]["token"] = check_for_attribute(self.data, "token", parent="radarr", default_is_none=True) if "radarr" in self.data else None
self.general["radarr"]["quality_profile"] = check_for_attribute(self.data, "quality_profile", parent="radarr", default_is_none=True) if "radarr" in self.data else None
self.general["radarr"]["root_folder_path"] = check_for_attribute(self.data, "root_folder_path", parent="radarr", default_is_none=True) if "radarr" in self.data else None
self.general["radarr"]["add"] = check_for_attribute(self.data, "add", parent="radarr", var_type="bool", default=False) if "radarr" in self.data else False
self.general["radarr"]["search"] = check_for_attribute(self.data, "search", parent="radarr", var_type="bool", default=False) if "radarr" in self.data else False
self.general["sonarr"] = {}
self.general["sonarr"]["url"] = check_for_attribute(self.data, "url", parent="sonarr", default_is_none=True) if "sonarr" in self.data else None
self.general["sonarr"]["token"] = check_for_attribute(self.data, "token", parent="sonarr", default_is_none=True) if "sonarr" in self.data else None
self.general["sonarr"]["version"] = check_for_attribute(self.data, "version", parent="sonarr", test_list=["v2", "v3"], options="| \tv2 (For Sonarr 0.2)\n| \tv3 (For Sonarr 3.0)", default="v2") if "sonarr" in self.data else "v2"
self.general["sonarr"]["quality_profile"] = check_for_attribute(self.data, "quality_profile", parent="sonarr", default_is_none=True) if "sonarr" in self.data else None
self.general["sonarr"]["root_folder_path"] = check_for_attribute(self.data, "root_folder_path", parent="sonarr", default_is_none=True) if "sonarr" in self.data else None
self.general["sonarr"]["add"] = check_for_attribute(self.data, "add", parent="sonarr", var_type="bool", default=False) if "sonarr" in self.data else False
self.general["sonarr"]["search"] = check_for_attribute(self.data, "search", parent="sonarr", var_type="bool", default=False) if "sonarr" in self.data else False
self.general["tautulli"] = {}
self.general["tautulli"]["url"] = check_for_attribute(self.data, "url", parent="tautulli", default_is_none=True) if "tautulli" in self.data else None
self.general["tautulli"]["apikey"] = check_for_attribute(self.data, "apikey", parent="tautulli", default_is_none=True) if "tautulli" in self.data else None
util.seperator()
logger.info("Connecting to Plex Libraries...")
self.libraries = []
libs = check_for_attribute(self.data, "libraries", throw=True)
for lib in libs:
util.seperator()
params = {}
if "library_name" in libs[lib] and libs[lib]["library_name"]:
params["name"] = str(libs[lib]["library_name"])
logger.info("Connecting to {} ({}) Library...".format(params["name"], lib))
else:
params["name"] = str(lib)
logger.info("Connecting to {} Library...".format(params["name"]))
default_lib = os.path.join(default_dir, "{}.yml".format(lib))
try:
if "metadata_path" in libs[lib]:
if libs[lib]["metadata_path"]:
if os.path.exists(libs[lib]["metadata_path"]): params["metadata_path"] = libs[lib]["metadata_path"]
else: raise Failed("metadata_path not found at {}".format(libs[lib]["metadata_path"]))
else: raise Failed("metadata_path attribute is blank")
else:
if os.path.exists(default_lib): params["metadata_path"] = os.path.abspath(default_lib)
else: raise Failed("default metadata_path not found at {}".format(os.path.abspath(os.path.join(default_dir, "{}.yml".format(params["name"])))))
if "library_type" in libs[lib]:
if libs[lib]["library_type"]:
if libs[lib]["library_type"] in ["movie", "show"]: params["library_type"] = libs[lib]["library_type"]
else: raise Failed("library_type attribute must be either 'movie' or 'show'")
else: raise Failed("library_type attribute is blank")
else: raise Failed("library_type attribute is required")
params["plex"] = {}
if "plex" in libs[lib] and libs[lib]["plex"] and "url" in libs[lib]["plex"]:
if libs[lib]["plex"]["url"]: params["plex"]["url"] = libs[lib]["plex"]["url"]
else: raise Failed("url library attribute is blank")
elif self.general["plex"]["url"]: params["plex"]["url"] = self.general["plex"]["url"]
else: raise Failed("url attribute must be set under plex or under this specific Library")
if "plex" in libs[lib] and libs[lib]["plex"] and "token" in libs[lib]["plex"]:
if libs[lib]["plex"]["token"]: params["plex"]["token"] = libs[lib]["plex"]["token"]
else: raise Failed("token library attribute is blank")
elif self.general["plex"]["token"]: params["plex"]["token"] = self.general["plex"]["token"]
else: raise Failed("token attribute must be set under plex or under this specific Library")
except Failed as e:
logger.error("Config Error: Skipping {} Library {}".format(str(lib), e))
continue
params["asset_directory"] = None
if "plex" in libs[lib] and "asset_directory" in libs[lib]["plex"]:
if libs[lib]["plex"]["asset_directory"]:
if os.path.exists(libs[lib]["plex"]["asset_directory"]):
params["asset_directory"] = libs[lib]["plex"]["asset_directory"]
else:
logger.warning("Config Warning: Assets will not be used asset_directory not found at {}".format(libs[lib]["plex"]["asset_directory"]))
else:
logger.warning("Config Warning: Assets will not be used asset_directory library attribute is blank")
elif self.general["plex"]["asset_directory"]:
params["asset_directory"] = self.general["plex"]["asset_directory"]
else:
logger.warning("Config Warning: Assets will not be used asset_directory attribute must be set under config or under this specific Library")
params["sync_mode"] = self.general["plex"]["sync_mode"]
if "plex" in libs[lib] and "sync_mode" in libs[lib]["plex"]:
if libs[lib]["plex"]["sync_mode"]:
if libs[lib]["plex"]["sync_mode"] in ["append", "sync"]:
params["sync_mode"] = libs[lib]["plex"]["sync_mode"]
else:
logger.warning("Config Warning: sync_mode attribute must be either 'append' or 'sync' using general value: {}".format(self.general["plex"]["sync_mode"]))
else:
logger.warning("Config Warning: sync_mode attribute is blank using general value: {}".format(self.general["plex"]["sync_mode"]))
params["show_unmanaged_collections"] = self.general["plex"]["show_unmanaged_collections"]
if "plex" in libs[lib] and "show_unmanaged_collections" in libs[lib]["plex"]:
if libs[lib]["plex"]["show_unmanaged_collections"]:
if isinstance(libs[lib]["plex"]["show_unmanaged_collections"], bool):
params["plex"]["show_unmanaged_collections"] = libs[lib]["plex"]["show_unmanaged_collections"]
else:
logger.warning("Config Warning: plex sub-attribute show_unmanaged_collections must be either true or false using general value: {}".format(self.general["plex"]["show_unmanaged_collections"]))
else:
logger.warning("Config Warning: radarr sub-attribute add is blank using general value: {}".format(self.general["radarr"]["add"]))
params["tmdb"] = self.TMDb
params["tvdb"] = self.TVDb
params["radarr"] = self.general["radarr"].copy()
if "radarr" in libs[lib] and libs[lib]["radarr"]:
if "url" in libs[lib]["radarr"]:
if libs[lib]["radarr"]["url"]:
params["radarr"]["url"] = libs[lib]["radarr"]["url"]
else:
logger.warning("Config Warning: radarr sub-attribute url is blank using general value: {}".format(self.general["radarr"]["url"]))
if "token" in libs[lib]["radarr"]:
if libs[lib]["radarr"]["token"]:
params["radarr"]["token"] = libs[lib]["radarr"]["token"]
else:
logger.warning("Config Warning: radarr sub-attribute token is blank using general value: {}".format(self.general["radarr"]["token"]))
if "version" in libs[lib]["radarr"]:
if libs[lib]["radarr"]["version"]:
if libs[lib]["radarr"]["version"] in ["v2", "v3"]:
params["radarr"]["version"] = libs[lib]["radarr"]["version"]
else:
logger.warning("Config Warning: radarr sub-attribute version must be either 'v2' or 'v3' using general value: {}".format(self.general["radarr"]["version"]))
else:
logger.warning("Config Warning: radarr sub-attribute version is blank using general value: {}".format(self.general["radarr"]["version"]))
if "quality_profile" in libs[lib]["radarr"]:
if libs[lib]["radarr"]["quality_profile"]:
params["radarr"]["quality_profile"] = libs[lib]["radarr"]["quality_profile"]
else:
logger.warning("Config Warning: radarr sub-attribute quality_profile is blank using general value: {}".format(self.general["radarr"]["quality_profile"]))
if "root_folder_path" in libs[lib]["radarr"]:
if libs[lib]["radarr"]["root_folder_path"]:
params["radarr"]["root_folder_path"] = libs[lib]["radarr"]["root_folder_path"]
else:
logger.warning("Config Warning: radarr sub-attribute root_folder_path is blank using general value: {}".format(self.general["radarr"]["root_folder_path"]))
if "add" in libs[lib]["radarr"]:
if libs[lib]["radarr"]["add"]:
if isinstance(libs[lib]["radarr"]["add"], bool):
params["radarr"]["add"] = libs[lib]["radarr"]["add"]
else:
logger.warning("Config Warning: radarr sub-attribute add must be either true or false using general value: {}".format(self.general["radarr"]["add"]))
else:
logger.warning("Config Warning: radarr sub-attribute add is blank using general value: {}".format(self.general["radarr"]["add"]))
if "search" in libs[lib]["radarr"]:
if libs[lib]["radarr"]["search"]:
if isinstance(libs[lib]["radarr"]["search"], bool):
params["radarr"]["search"] = libs[lib]["radarr"]["search"]
else:
logger.warning("Config Warning: radarr sub-attribute search must be either true or false using general value: {}".format(self.general["radarr"]["search"]))
else:
logger.warning("Config Warning: radarr sub-attribute search is blank using general value: {}".format(self.general["radarr"]["search"]))
if not params["radarr"]["url"] or not params["radarr"]["token"] or not params["radarr"]["quality_profile"] or not params["radarr"]["root_folder_path"]:
params["radarr"] = None
params["sonarr"] = self.general["sonarr"].copy()
if "sonarr" in libs[lib] and libs[lib]["sonarr"]:
if "url" in libs[lib]["sonarr"]:
if libs[lib]["sonarr"]["url"]:
params["sonarr"]["url"] = libs[lib]["sonarr"]["url"]
else:
logger.warning("Config Warning: sonarr sub-attribute url is blank using general value: {}".format(self.general["sonarr"]["url"]))
if "token" in libs[lib]["sonarr"]:
if libs[lib]["sonarr"]["token"]:
params["sonarr"]["token"] = libs[lib]["sonarr"]["token"]
else:
logger.warning("Config Warning: sonarr sub-attribute token is blank using general value: {}".format(self.general["sonarr"]["token"]))
if "version" in libs[lib]["sonarr"]:
if libs[lib]["sonarr"]["version"]:
if libs[lib]["sonarr"]["version"] in ["v2", "v3"]:
params["sonarr"]["version"] = libs[lib]["sonarr"]["version"]
else:
logger.warning("Config Warning: sonarr sub-attribute version must be either 'v2' or 'v3' using general value: {}".format(self.general["sonarr"]["version"]))
else:
logger.warning("Config Warning: sonarr sub-attribute version is blank using general value: {}".format(self.general["sonarr"]["version"]))
if "quality_profile" in libs[lib]["sonarr"]:
if libs[lib]["sonarr"]["quality_profile"]:
params["sonarr"]["quality_profile"] = libs[lib]["sonarr"]["quality_profile"]
else:
logger.warning("Config Warning: sonarr sub-attribute quality_profile is blank using general value: {}".format(self.general["sonarr"]["quality_profile"]))
if "root_folder_path" in libs[lib]["sonarr"]:
if libs[lib]["sonarr"]["root_folder_path"]:
params["sonarr"]["root_folder_path"] = libs[lib]["sonarr"]["root_folder_path"]
else:
logger.warning("Config Warning: sonarr sub-attribute root_folder_path is blank using general value: {}".format(self.general["sonarr"]["root_folder_path"]))
if "add" in libs[lib]["sonarr"]:
if libs[lib]["sonarr"]["add"]:
if isinstance(libs[lib]["sonarr"]["add"], bool):
params["sonarr"]["add"] = libs[lib]["sonarr"]["add"]
else:
logger.warning("Config Warning: sonarr sub-attribute add must be either true or false using general value: {}".format(self.general["sonarr"]["add"]))
else:
logger.warning("Config Warning: sonarr sub-attribute add is blank using general value: {}".format(self.general["sonarr"]["add"]))
if "search" in libs[lib]["sonarr"]:
if libs[lib]["sonarr"]["search"]:
if isinstance(libs[lib]["sonarr"]["search"], bool):
params["sonarr"]["search"] = libs[lib]["sonarr"]["search"]
else:
logger.warning("Config Warning: sonarr sub-attribute search must be either true or false using general value: {}".format(self.general["sonarr"]["search"]))
else:
logger.warning("Config Warning: sonarr sub-attribute search is blank using general value: {}".format(self.general["sonarr"]["search"]))
if not params["sonarr"]["url"] or not params["sonarr"]["token"] or not params["sonarr"]["quality_profile"] or not params["sonarr"]["root_folder_path"] or params["library_type"] == "movie":
params["sonarr"] = None
params["tautulli"] = self.general["tautulli"].copy()
if "tautulli" in libs[lib] and libs[lib]["tautulli"]:
if "url" in libs[lib]["tautulli"]:
if libs[lib]["tautulli"]["url"]:
params["tautulli"]["url"] = libs[lib]["tautulli"]["url"]
else:
logger.warning("Config Warning: tautulli sub-attribute url is blank using general value: {}".format(self.general["tautulli"]["url"]))
if "apikey" in libs[lib]["tautulli"]:
if libs[lib]["tautulli"]["apikey"]:
params["tautulli"]["apikey"] = libs[lib]["tautulli"]["apikey"]
else:
logger.warning("Config Warning: tautulli sub-attribute apikey is blank using general value: {}".format(self.general["tautulli"]["apikey"]))
if not params["tautulli"]["url"] or not params["tautulli"]["apikey"] :
params["tautulli"] = None
try:
self.libraries.append(PlexAPI(params))
logger.info("{} Library Connection Successful".format(params["name"]))
except Failed as e:
logger.error(e)
logger.info("{} Library Connection Failed".format(params["name"]))
continue
util.seperator()
if len(self.libraries) > 0:
logger.info("{} Plex Library Connection{} Successful".format(len(self.libraries), "s" if len(self.libraries) > 1 else ""))
else:
raise Failed("Plex Error: No Plex libraries were found")
util.seperator()
def update_libraries(self):
for library in self.libraries:
logger.info("")
util.seperator("{} Library".format(library.name))
try: library.update_metadata(self.TMDb)
except Failed as e: logger.error(e)
logger.info("")
util.seperator("{} Library Collections".format(library.name))
collections = library.collections
if collections:
logger.info("")
util.seperator("Mapping {} Library".format(library.name))
logger.info("")
movie_map, show_map = self.map_guids(library)
for c in collections:
try:
logger.info("")
util.seperator("{} Collection".format(c))
logger.info("")
map = {}
details = {}
methods = []
filters = []
posters_found = []
backgrounds_found = []
collectionless = "plex_collectionless" in collections[c]
skip_collection = True
if "schedule" not in collections[c]:
skip_collection = False
elif not collections[c]["schedule"]:
logger.error("Collection Error: schedule attribute is blank. Running daily")
skip_collection = False
else:
schedule_list = util.get_list(collections[c]["schedule"])
current_time = datetime.now()
next_month = current_time.replace(day=28) + timedelta(days=4)
last_day = next_month - timedelta(days=next_month.day)
for schedule in schedule_list:
run_time = str(schedule).lower()
if run_time.startswith("day") or run_time.startswith("daily"):
skip_collection = False
break
if run_time.startswith("week") or run_time.startswith("month") or run_time.startswith("year"):
match = re.search("\\(([^)]+)\\)", run_time)
if match:
param = match.group(1)
if run_time.startswith("week"):
if param.lower() in util.days_alias:
weekday = util.days_alias[param.lower()]
logger.info("Scheduled weekly on {}".format(util.pretty_days[weekday]))
if weekday == current_time.weekday():
skip_collection = False
break
else:
logger.error("Collection Error: weekly schedule attribute {} invalid must be a day of the weeek i.e. weekly(Monday)".format(schedule))
elif run_time.startswith("month"):
try:
if 1 <= int(param) <= 31:
logger.info("Scheduled monthly on the {}".format(util.make_ordinal(param)))
if current_time.day == int(param) or (current_time.day == last_day.day and int(param) > last_day.day):
skip_collection = False
break
else:
logger.error("Collection Error: monthly schedule attribute {} invalid must be between 1 and 31".format(schedule))
except ValueError:
logger.error("Collection Error: monthly schedule attribute {} invalid must be an integer".format(schedule))
elif run_time.startswith("year"):
match = re.match("^(1[0-2]|0?[1-9])/(3[01]|[12][0-9]|0?[1-9])$", param)
if match:
month = int(match.group(1))
day = int(match.group(2))
logger.info("Scheduled yearly on {} {}".format(util.pretty_months[month], util.make_ordinal(day)))
if current_time.month == month and (current_time.day == day or (current_time.day == last_day.day and day > last_day.day)):
skip_collection = False
break
else:
logger.error("Collection Error: yearly schedule attribute {} invalid must be in the MM/DD format i.e. yearly(11/22)".format(schedule))
else:
logger.error("Collection Error: failed to parse schedule: {}".format(schedule))
else:
logger.error("Collection Error: schedule attribute {} invalid".format(schedule))
if skip_collection:
logger.info("Skipping Collection {}".format(c))
continue
try:
collection_obj = library.get_collection(c)
collection_name = collection_obj.title
except Failed as e:
collection_obj = None
collection_name = c
sync_collection = library.sync_mode == "sync"
if "sync_mode" in collections[c]:
if not collections[c]["sync_mode"]: logger.warning("Collection Warning: sync_mode attribute is blank using general: {}".format(library.sync_mode))
elif collections[c]["sync_mode"] not in ["append", "sync"]: logger.warning("Collection Warning: {} sync_mode invalid using general: {}".format(library.sync_mode, collections[c]["sync_mode"]))
else: sync_collection = collections[c]["sync_mode"] == "sync"
if sync_collection or collectionless:
logger.info("Sync Mode: sync")
if collection_obj:
for item in collection_obj.items():
map[item.ratingKey] = item
else:
logger.info("Sync Mode: append")
if "tmdb_person" in collections[c]:
if collections[c]["tmdb_person"]:
valid_names = []
for tmdb_id in util.get_int_list(collections[c]["tmdb_person"], "TMDb Person ID"):
try:
person = self.TMDb.get_person(tmdb_id)
valid_names.append(person.name)
if "summary" not in details and hasattr(person, "biography") and person.biography:
details["summary"] = person.biography
if "poster" not in details and hasattr(person, "profile_path") and person.profile_path:
details["poster"] = ("url", "{}{}".format(self.TMDb.image_url, person.profile_path), "tmdb_person")
except Failed as e:
util.print_stacktrace()
logger.error(e)
if len(valid_names) > 0: details["tmdb_person"] = valid_names
else: logger.error("Collection Error: No valid TMDb Person IDs in {}".format(collections[c]["tmdb_person"]))
else:
logger.error("Collection Error: tmdb_person attribute is blank")
for m in collections[c]:
try:
if "tmdb" in m and not self.TMDb:
logger.info("Collection Error: {} skipped. TMDb must be configured".format(m))
map = {}
elif "trakt" in m and not self.Trakt:
logger.info("Collection Error: {} skipped. Trakt must be configured".format(m))
map = {}
elif "imdb" in m and not self.IMDb:
logger.info("Collection Error: {} skipped. TMDb or Trakt must be configured".format(m))
map = {}
elif "tautulli" in m and not library.Tautulli:
logger.info("Collection Error: {} skipped. Tautulli must be configured".format(m))
map = {}
elif "mal" in m and not self.MyAnimeList:
logger.info("Collection Error: {} skipped. MyAnimeList must be configured".format(m))
map = {}
elif collections[c][m] is not None:
logger.debug("Method: {}".format(m))
logger.debug("Value: {}".format(collections[c][m]))
if m in util.method_alias:
method_name = util.method_alias[m]
logger.warning("Collection Warning: {} attribute will run as {}".format(m, method_name))
else:
method_name = m
if method_name in util.show_only_lists and library.is_movie: raise Failed("Collection Error: {} attribute only works for show libraries".format(method_name))
elif method_name in util.movie_only_lists and library.is_show: raise Failed("Collection Error: {} attribute only works for movie libraries".format(method_name))
elif method_name in util.movie_only_searches and library.is_show: raise Failed("Collection Error: {} plex search only works for movie libraries".format(method_name))
elif method_name not in util.collectionless_lists and collectionless: raise Failed("Collection Error: {} attribute does not work for Collectionless collection".format(method_name))
elif method_name == "tmdb_summary": details["summary"] = self.TMDb.get_movie_show_or_collection(util.regex_first_int(collections[c][m], "TMDb ID"), library.is_movie).overview
elif method_name == "tmdb_description": details["summary"] = self.TMDb.get_list(util.regex_first_int(collections[c][m], "TMDb List ID")).description
elif method_name == "tmdb_biography": details["summary"] = self.TMDb.get_person(util.regex_first_int(collections[c][m], "TMDb Person ID")).biography
elif method_name == "collection_mode":
if collections[c][m] in ["default", "hide", "hide_items", "show_items", "hideItems", "showItems"]:
if collections[c][m] == "hide_items": details[method_name] = "hideItems"
elif collections[c][m] == "show_items": details[method_name] = "showItems"
else: details[method_name] = collections[c][m]
else: raise Failed("Collection Error: {} collection_mode Invalid\n| \tdefault (Library default)\n| \thide (Hide Collection)\n| \thide_items (Hide Items in this Collection)\n| \tshow_items (Show this Collection and its Items)".format(collections[c][m]))
elif method_name == "collection_order":
if collections[c][m] in ["release", "alpha"]: details[method_name] = collections[c][m]
else: raise Failed("Collection Error: {} collection_order Invalid\n| \trelease (Order Collection by release dates)\n| \talpha (Order Collection Alphabetically)".format(collections[c][m]))
elif method_name == "url_poster": posters_found.append(("url", collections[c][m], method_name))
elif method_name == "tmdb_poster": posters_found.append(("url", "{}{}".format(self.TMDb.image_url, self.TMDb.get_movie_show_or_collection(util.regex_first_int(collections[c][m], "TMDb ID"), library.is_movie).poster_path), method_name))
elif method_name == "tmdb_profile": posters_found.append(("url", "{}{}".format(self.TMDb.image_url, self.TMDb.get_person(util.regex_first_int(collections[c][m], "TMDb Person ID")).profile_path), method_name))
elif method_name == "file_poster":
if os.path.exists(collections[c][m]): posters_found.append(("file", os.path.abspath(collections[c][m]), method_name))
else: raise Failed("Collection Error: Poster Path Does Not Exist: {}".format(os.path.abspath(collections[c][m])))
elif method_name == "url_background": backgrounds_found.append(("url", collections[c][m], method_name))
elif method_name == "tmdb_background": backgrounds_found.append(("url", "{}{}".format(self.TMDb.image_url, self.TMDb.get_movie_show_or_collection(util.regex_first_int(collections[c][m], "TMDb ID"), library.is_movie).poster_path), method_name))
elif method_name == "file_background":
if os.path.exists(collections[c][m]): backgrounds_found.append(("file", os.path.abspath(collections[c][m]), method_name))
else: raise Failed("Collection Error: Background Path Does Not Exist: {}".format(os.path.abspath(collections[c][m])))
elif method_name == "add_to_arr":
if isinstance(collections[c][m], bool): details[method_name] = collections[c][m]
else: raise Failed("Collection Error: add_to_arr must be either true or false")
elif method_name in util.all_details: details[method_name] = collections[c][m]
elif method_name in ["year", "year.not"]: methods.append(("plex_search", [[(method_name, util.get_year_list(collections[c][m], method_name))]]))
elif method_name in ["decade", "decade.not"]: methods.append(("plex_search", [[(method_name, util.get_int_list(collections[c][m], util.remove_not(method_name)))]]))
elif method_name in util.tmdb_searches:
final_values = []
for value in util.get_list(collections[c][m]):
if value.lower() == "tmdb" and "tmdb_person" in details:
for name in details["tmdb_person"]:
final_values.append(name)
else:
final_values.append(value)
methods.append(("plex_search", [[(method_name, final_values)]]))
elif method_name in util.plex_searches: methods.append(("plex_search", [[(method_name, util.get_list(collections[c][m]))]]))
elif method_name == "plex_all": methods.append((method_name, [""]))
elif method_name == "plex_collection": methods.append((method_name, library.validate_collections(collections[c][m] if isinstance(collections[c][m], list) else [collections[c][m]])))
elif method_name == "anidb_popular":
list_count = util.regex_first_int(collections[c][m], "List Size", default=40)
if 1 <= list_count <= 30:
methods.append((method_name, [list_count]))
else:
logger.error("Collection Error: anidb_popular must be an integer between 1 and 30 defaulting to 30")
methods.append((method_name, [30]))
elif method_name == "mal_id": methods.append((method_name, util.get_int_list(collections[c][m], "MyAnimeList ID")))
elif method_name in ["anidb_id", "anidb_relation"]: methods.append((method_name, self.AniDB.validate_anidb_list(util.get_int_list(collections[c][m], "AniDB ID"), library.Plex.language)))
elif method_name == "trakt_list": methods.append((method_name, self.Trakt.validate_trakt_list(util.get_list(collections[c][m]))))
elif method_name == "trakt_watchlist": methods.append((method_name, self.Trakt.validate_trakt_watchlist(util.get_list(collections[c][m]), library.is_movie)))
elif method_name == "imdb_list":
new_list = []
for imdb_list in util.get_list(collections[c][m]):
new_dictionary = {}
if isinstance(imdb_list, dict):
if "url" in imdb_list and imdb_list["url"]: imdb_url = imdb_list["url"]
else: raise Failed("Collection Error: No I")
if "limit" in imdb_list and imdb_list["limit"]: list_count = util.regex_first_int(imdb_list["limit"], "List Limit", default=0)
else: list_count = 0
else:
imdb_url = str(imdb_list)
list_count = 0
new_list.append({"url": imdb_url, "limit": list_count})
methods.append((method_name, new_list))
elif method_name in util.dictionary_lists:
if isinstance(collections[c][m], dict):
def get_int(parent, method, data, default, min=1, max=None):
if method not in data: logger.warning("Collection Warning: {} {} attribute not found using {} as default".format(parent, method, default))
elif not data[method]: logger.warning("Collection Warning: {} {} attribute is blank using {} as default".format(parent, method, default))
elif isinstance(data[method], int) and data[method] >= min:
if max is None or data[method] <= max: return data[method]
else: logger.warning("Collection Warning: {} {} attribute {} invalid must an integer <= {} using {} as default".format(parent, method, data[method], max, default))
else: logger.warning("Collection Warning: {} {} attribute {} invalid must an integer >= {} using {} as default".format(parent, method, data[method], min, default))
return default
if method_name == "filters":
for filter in collections[c][m]:
if filter in util.method_alias or (filter.endswith(".not") and filter[:-4] in util.method_alias):
final_filter = (util.method_alias[filter[:-4]] + filter[-4:]) if filter.endswith(".not") else util.method_alias[filter]
logger.warning("Collection Warning: {} filter will run as {}".format(filter, final_filter))
else:
final_filter = filter
if final_filter in util.movie_only_filters and library.is_show:
logger.error("Collection Error: {} filter only works for movie libraries".format(final_filter))
elif final_filter in util.all_filters:
filters.append((final_filter, collections[c][m][filter]))
else:
logger.error("Collection Error: {} filter not supported".format(filter))
elif method_name == "plex_collectionless":
new_dictionary = {}
prefix_list = []
if "exclude_prefix" in collections[c][m] and collections[c][m]["exclude_prefix"]:
if isinstance(collections[c][m]["exclude_prefix"], list):
prefix_list.extend(collections[c][m]["exclude_prefix"])
else:
prefix_list.append("{}".format(collections[c][m]["exclude_prefix"]))
exact_list = []
if "exclude" in collections[c][m] and collections[c][m]["exclude"]:
if isinstance(collections[c][m]["exclude"], list):
exact_list.extend(collections[c][m]["exclude"])
else:
exact_list.append("{}".format(collections[c][m]["exclude"]))
if len(prefix_list) == 0 and len(exact_list) == 0:
raise Failed("Collection Error: you must have at least one exclusion")
details["add_to_arr"] = False
details["collection_mode"] = "hide"
new_dictionary["exclude_prefix"] = prefix_list
new_dictionary["exclude"] = exact_list
methods.append((method_name, [new_dictionary]))
elif method_name == "plex_search":
search = []
searches_used = []
for search_attr in collections[c][m]:
if search_attr in util.method_alias or (search_attr.endswith(".not") and search_attr[:-4] in util.method_alias):
final_attr = (util.method_alias[search_attr[:-4]] + search_attr[-4:]) if search_attr.endswith(".not") else util.method_alias[search_attr]
logger.warning("Collection Warning: {} plex search attribute will run as {}".format(search_attr, final_attr))
else:
final_attr = search_attr
if final_attr in util.movie_only_searches and library.is_show:
logger.error("Collection Error: {} plex search attribute only works for movie libraries".format(final_attr))
elif util.remove_not(final_attr) in searches_used:
logger.error("Collection Error: Only one instance of {} can be used try using it as a filter instead".format(final_attr))
elif final_attr in ["year", "year.not"]:
years = util.get_year_list(collections[c][m][search_attr], final_attr)
if len(years) > 0:
searches_used.append(util.remove_not(final_attr))
search.append((final_attr, util.get_int_list(collections[c][m][search_attr], util.remove_not(final_attr))))
elif final_attr in util.plex_searches:
if final_attr.startswith("tmdb_"):
final_attr = final_attr[5:]
searches_used.append(util.remove_not(final_attr))
search.append((final_attr, util.get_list(collections[c][m][search_attr])))
else:
logger.error("Collection Error: {} plex search attribute not supported".format(search_attr))
methods.append((method_name, [search]))
elif method_name == "tmdb_discover":
new_dictionary = {"limit": 100}
for attr in collections[c][m]:
if collections[c][m][attr]:
attr_data = collections[c][m][attr]
if (library.is_movie and attr in util.discover_movie) or (library.is_show and attr in util.discover_tv):
if attr == "language":
if re.compile("([a-z]{2})-([A-Z]{2})").match(str(attr_data)):
new_dictionary[attr] = str(attr_data)
else:
logger.error("Collection Error: Skipping {} attribute {}: {} must match pattern ([a-z]{2})-([A-Z]{2}) e.g. en-US".format(m, attr, attr_data))
elif attr == "region":
if re.compile("^[A-Z]{2}$").match(str(attr_data)):
new_dictionary[attr] = str(attr_data)
else:
logger.error("Collection Error: Skipping {} attribute {}: {} must match pattern ^[A-Z]{2}$ e.g. US".format(m, attr, attr_data))
elif attr == "sort_by":
if (library.is_movie and attr_data in util.discover_movie_sort) or (library.is_show and attr_data in util.discover_tv_sort):
new_dictionary[attr] = attr_data
else:
logger.error("Collection Error: Skipping {} attribute {}: {} is invalid".format(m, attr, attr_data))
elif attr == "certification_country":
if "certification" in collections[c][m] or "certification.lte" in collections[c][m] or "certification.gte" in collections[c][m]:
new_dictionary[attr] = attr_data
else:
logger.error("Collection Error: Skipping {} attribute {}: must be used with either certification, certification.lte, or certification.gte".format(m, attr))
elif attr in ["certification", "certification.lte", "certification.gte"]:
if "certification_country" in collections[c][m]:
new_dictionary[attr] = attr_data
else:
logger.error("Collection Error: Skipping {} attribute {}: must be used with certification_country".format(m, attr))
elif attr in ["include_adult", "include_null_first_air_dates", "screened_theatrically"]:
if attr_data is True:
new_dictionary[attr] = attr_data
elif attr in ["primary_release_date.gte", "primary_release_date.lte", "release_date.gte", "release_date.lte", "air_date.gte", "air_date.lte", "first_air_date.gte", "first_air_date.lte"]:
if re.compile("[0-1]?[0-9][/-][0-3]?[0-9][/-][1-2][890][0-9][0-9]").match(str(attr_data)):
the_date = str(attr_data).split("/") if "/" in str(attr_data) else str(attr_data).split("-")
new_dictionary[attr] = "{}-{}-{}".format(the_date[2], the_date[0], the_date[1])
elif re.compile("[1-2][890][0-9][0-9][/-][0-1]?[0-9][/-][0-3]?[0-9]").match(str(attr_data)):
the_date = str(attr_data).split("/") if "/" in str(attr_data) else str(attr_data).split("-")
new_dictionary[attr] = "{}-{}-{}".format(the_date[0], the_date[1], the_date[2])
else:
logger.error("Collection Error: Skipping {} attribute {}: {} must match pattern MM/DD/YYYY e.g. 12/25/2020".format(m, attr, attr_data))
elif attr in ["primary_release_year", "year", "first_air_date_year"]:
if isinstance(attr_data, int) and 1800 < attr_data and attr_data < 2200:
new_dictionary[attr] = attr_data
else:
logger.error("Collection Error: Skipping {} attribute {}: must be a valid year e.g. 1990".format(m, attr))
elif attr in ["vote_count.gte", "vote_count.lte", "vote_average.gte", "vote_average.lte", "with_runtime.gte", "with_runtime.lte"]:
if (isinstance(attr_data, int) or isinstance(attr_data, float)) and 0 < attr_data:
new_dictionary[attr] = attr_data
else:
logger.error("Collection Error: Skipping {} attribute {}: must be a valid number greater then 0".format(m, attr))
elif attr in ["with_cast", "with_crew", "with_people", "with_companies", "with_networks", "with_genres", "without_genres", "with_keywords", "without_keywords", "with_original_language", "timezone"]:
new_dictionary[attr] = attr_data
else:
logger.error("Collection Error: {} attribute {} not supported".format(m, attr))
elif attr == "limit":
if isinstance(attr_data, int) and attr_data > 0:
new_dictionary[attr] = attr_data
else:
logger.error("Collection Error: Skipping {} attribute {}: must be a valid number greater then 0".format(m, attr))
else:
logger.error("Collection Error: {} attribute {} not supported".format(m, attr))
else:
logger.error("Collection Error: {} parameter {} is blank".format(m, attr))
if len(new_dictionary) > 1:
methods.append((method_name, [new_dictionary]))
else:
logger.error("Collection Error: {} had no valid fields".format(m))
elif "tautulli" in method_name:
new_dictionary = {}
if method_name == "tautulli_popular": new_dictionary["list_type"] = "popular"
elif method_name == "tautulli_watched": new_dictionary["list_type"] = "watched"
else: raise Failed("Collection Error: {} attribute not supported".format(method_name))
new_dictionary["list_days"] = get_int(method_name, "list_days", collections[c][m], 30)
new_dictionary["list_size"] = get_int(method_name, "list_size", collections[c][m], 10)
new_dictionary["list_buffer"] = get_int(method_name, "list_buffer", collections[c][m], 20)
methods.append((method_name, [new_dictionary]))
elif method_name == "mal_season":
new_dictionary = {"sort_by": "anime_num_list_users"}
current_time = datetime.now()
if current_time.month in [1, 2, 3]: new_dictionary["season"] = "winter"
elif current_time.month in [4, 5, 6]: new_dictionary["season"] = "spring"
elif current_time.month in [7, 8, 9]: new_dictionary["season"] = "summer"
elif current_time.month in [10, 11, 12]: new_dictionary["season"] = "fall"
new_dictionary["year"] = get_int(method_name, "year", collections[c][m], current_time.year, min=1917, max=current_time.year + 1)
new_dictionary["limit"] = get_int(method_name, "limit", collections[c][m], 100, max=500)
if "sort_by" not in collections[c][m]: logger.warning("Collection Error: mal_season sort_by attribute not found using members as default")
elif not collections[c][m]["sort_by"]: logger.warning("Collection Error: mal_season sort_by attribute is blank using members as default")
elif collections[c][m]["sort_by"] not in util.mal_season_sort: logger.warning("Collection Error: mal_season sort_by attribute {} invalid must be either 'members' or 'score' using members as default".format(collections[c][m]["sort_by"]))
else: new_dictionary["sort_by"] = util.mal_season_sort[collections[c][m]["sort_by"]]
if "season" not in collections[c][m]: logger.warning("Collection Error: mal_season season attribute not found using the current season: {} as default".format(new_dictionary["season"]))
elif not collections[c][m]["season"]: logger.warning("Collection Error: mal_season season attribute is blank using the current season: {} as default".format(new_dictionary["season"]))
elif collections[c][m]["season"] not in util.pretty_seasons: logger.warning("Collection Error: mal_season season attribute {} invalid must be either 'winter', 'spring', 'summer' or 'fall' using the current season: {} as default".format(collections[c][m]["season"], new_dictionary["season"]))
else: new_dictionary["season"] = collections[c][m]["season"]
methods.append((method_name, [new_dictionary]))
elif method_name == "mal_userlist":
new_dictionary = {"status": "all", "sort_by": "list_score"}
if "username" not in collections[c][m]: raise Failed("Collection Error: mal_userlist username attribute is required")
elif not collections[c][m]["username"]: raise Failed("Collection Error: mal_userlist username attribute is blank")
else: new_dictionary["username"] = collections[c][m]["username"]
if "status" not in collections[c][m]: logger.warning("Collection Error: mal_season status attribute not found using all as default")
elif not collections[c][m]["status"]: logger.warning("Collection Error: mal_season status attribute is blank using all as default")
elif collections[c][m]["status"] not in util.mal_userlist_status: logger.warning("Collection Error: mal_season status attribute {} invalid must be either 'all', 'watching', 'completed', 'on_hold', 'dropped' or 'plan_to_watch' using all as default".format(collections[c][m]["status"]))
else: new_dictionary["status"] = util.mal_userlist_status[collections[c][m]["status"]]
if "sort_by" not in collections[c][m]: logger.warning("Collection Error: mal_season sort_by attribute not found using score as default")
elif not collections[c][m]["sort_by"]: logger.warning("Collection Error: mal_season sort_by attribute is blank using score as default")
elif collections[c][m]["sort_by"] not in util.mal_userlist_sort: logger.warning("Collection Error: mal_season sort_by attribute {} invalid must be either 'score', 'last_updated', 'title' or 'start_date' using score as default".format(collections[c][m]["sort_by"]))
else: new_dictionary["sort_by"] = util.mal_userlist_sort[collections[c][m]["sort_by"]]
new_dictionary["limit"] = get_int(method_name, "limit", collections[c][m], 100, max=1000)
methods.append((method_name, [new_dictionary]))
else:
logger.error("Collection Error: {} attribute is not a dictionary: {}".format(m, collections[c][m]))
elif method_name in util.count_lists:
list_count = util.regex_first_int(collections[c][m], "List Size", default=20)
if list_count > 0:
methods.append((method_name, [list_count]))
else:
logger.error("Collection Error: {} must be an integer greater then 0 defaulting to 20".format(method_name))
methods.append((method_name, [20]))
elif method_name in util.tmdb_lists:
values = self.TMDb.validate_tmdb_list(util.get_int_list(collections[c][m], "TMDb {} ID".format(util.tmdb_type[method_name])), util.tmdb_type[method_name])
if method_name[-8:] == "_details":
if method_name in ["tmdb_collection_details", "tmdb_movie_details", "tmdb_show_details"]:
item = self.TMDb.get_movie_show_or_collection(values[0], library.is_movie)
if "summary" not in details and hasattr(item, "overview") and item.overview:
details["summary"] = item.overview
if "background" not in details and hasattr(item, "backdrop_path") and item.backdrop_path:
details["background"] = ("url", "{}{}".format(self.TMDb.image_url, item.backdrop_path), method_name[:-8])
if "poster" not in details and hasattr(item, "poster_path") and item.poster_path:
details["poster"] = ("url", "{}{}".format(self.TMDb.image_url, item.poster_path), method_name[:-8])
else:
item = self.TMDb.get_list(values[0])
if "summary" not in details and hasattr(item, "description") and item.description:
details["summary"] = item.description
methods.append((method_name[:-8], values))
else:
methods.append((method_name, values))
elif method_name in util.all_lists: methods.append((method_name, util.get_list(collections[c][m])))
elif method_name not in ["sync_mode", "schedule", "tmdb_person"]: logger.error("Collection Error: {} attribute not supported".format(method_name))
else:
logger.error("Collection Error: {} attribute is blank".format(m))
except Failed as e:
logger.error(e)
for i, f in enumerate(filters):
if i == 0:
logger.info("")
logger.info("Collection Filter {}: {}".format(f[0], f[1]))
do_arr = False
if library.Radarr:
do_arr = details["add_to_arr"] if "add_to_arr" in details else library.Radarr.add
if library.Sonarr:
do_arr = details["add_to_arr"] if "add_to_arr" in details else library.Sonarr.add
items_found = 0
library.clear_collection_missing(collection_name)
for method, values in methods:
logger.debug("Method: {}".format(method))
logger.debug("Values: {}".format(values))
pretty = util.pretty_names[method] if method in util.pretty_names else method
for value in values:
logger.debug("Value: {}".format(value))
items = []
missing_movies = []
missing_shows = []
def check_map(input_ids):
movie_ids, show_ids = input_ids
items_found_inside = 0
if len(movie_ids) > 0:
items_found_inside += len(movie_ids)
for movie_id in movie_ids:
if movie_id in movie_map: items.append(movie_map[movie_id])
else: missing_movies.append(movie_id)
if len(show_ids) > 0:
items_found_inside += len(show_ids)
for show_id in show_ids:
if show_id in show_map: items.append(show_map[show_id])
else: missing_shows.append(show_id)
return items_found_inside
logger.info("")
if method == "plex_all":
logger.info("Processing {} {}".format(pretty, "Movies" if library.is_movie else "Shows"))
items = library.Plex.all()
items_found += len(items)
elif method == "plex_collection":
items = value.items()
items_found += len(items)
elif method == "plex_search":
search_terms = {}
output = ""
for i, attr_pair in enumerate(value):
search_list = attr_pair[1]
final_method = attr_pair[0][:-4] + "!" if attr_pair[0][-4:] == ".not" else attr_pair[0]
if library.is_show:
final_method = "show." + final_method
search_terms[final_method] = search_list
ors = ""
for o, param in enumerate(attr_pair[1]):
ors += "{}{}".format(" OR " if o > 0 else "{}(".format(attr_pair[0]), param)
logger.info("\t\t AND {})".format(ors) if i > 0 else "Processing {}: {})".format(pretty, ors))
items = library.Plex.search(**search_terms)
items_found += len(items)
elif method == "plex_collectionless":
good_collections = []
for col in library.get_all_collections():
keep_collection = True
for pre in value["exclude_prefix"]:
if col.title.startswith(pre) or (col.titleSort and col.titleSort.startswith(pre)):
keep_collection = False
break
for ext in value["exclude"]:
if col.title == ext or (col.titleSort and col.titleSort == ext):
keep_collection = False
break
if keep_collection:
good_collections.append(col.title.lower())
all_items = library.Plex.all()
length = 0
for i, item in enumerate(all_items, 1):
length = util.print_return(length, "Processing: {}/{} {}".format(i, len(all_items), item.title))
add_item = True
for collection in item.collections:
if collection.tag.lower() in good_collections:
add_item = False
break
if add_item:
items.append(item)
items_found += len(items)
util.print_end(length, "Processed {} {}".format(len(all_items), "Movies" if library.is_movie else "Shows"))
elif "tautulli" in method:
items = library.Tautulli.get_items(library, time_range=value["list_days"], stats_count=value["list_size"], list_type=value["list_type"], stats_count_buffer=value["list_buffer"])
items_found += len(items)
elif "anidb" in method: items_found += check_map(self.AniDB.get_items(method, value, library.Plex.language))
elif "mal" in method: items_found += check_map(self.MyAnimeList.get_items(method, value))
elif "tvdb" in method: items_found += check_map(self.TVDb.get_items(method, value, library.Plex.language))
elif "imdb" in method: items_found += check_map(self.IMDb.get_items(method, value, library.Plex.language))
elif "tmdb" in method: items_found += check_map(self.TMDb.get_items(method, value, library.is_movie))
elif "trakt" in method: items_found += check_map(self.Trakt.get_items(method, value, library.is_movie))
else: logger.error("Collection Error: {} method not supported".format(method))
if len(items) > 0: map = library.add_to_collection(collection_obj if collection_obj else collection_name, items, filters, map=map)
else: logger.error("No items found to add to this collection ")
if len(missing_movies) > 0 or len(missing_shows) > 0:
logger.info("")
if len(missing_movies) > 0:
missing_movies_with_names = []
for missing_id in missing_movies:
try:
title = str(self.TMDb.get_movie(missing_id).title)
missing_movies_with_names.append((title, missing_id))
logger.info("{} Collection | ? | {} (TMDb: {})".format(collection_name, title, missing_id))
except Failed as e:
logger.error(e)
logger.info("{} Movie{} Missing".format(len(missing_movies), "s" if len(missing_movies) > 1 else ""))
library.save_missing(collection_name, missing_movies_with_names, True)
if do_arr and library.Radarr:
library.Radarr.add_tmdb(missing_movies)
if len(missing_shows) > 0 and library.is_show:
missing_shows_with_names = []
for missing_id in missing_shows:
try:
title = str(self.TVDb.get_series(library.Plex.language, tvdb_id=missing_id).title.encode("ascii", "replace").decode())
missing_shows_with_names.append((title, missing_id))
logger.info("{} Collection | ? | {} (TVDB: {})".format(collection_name, title, missing_id))
except Failed as e:
logger.error(e)
logger.info("{} Show{} Missing".format(len(missing_shows), "s" if len(missing_shows) > 1 else ""))
library.save_missing(c, missing_shows_with_names, False)
if do_arr and library.Sonarr:
library.Sonarr.add_tvdb(missing_shows)
library.del_collection_if_empty(collection_name)
if (sync_collection or collectionless) and items_found > 0:
logger.info("")
count_removed = 0
for ratingKey, item in map.items():
if item is not None:
logger.info("{} Collection | - | {}".format(collection_name, item.title))
item.removeCollection(collection_name)
count_removed += 1
logger.info("{} {}{} Removed".format(count_removed, "Movie" if library.is_movie else "Show", "s" if count_removed == 1 else ""))
logger.info("")
try:
plex_collection = library.get_collection(collection_name)
except Failed as e:
logger.debug(e)
continue
edits = {}
if "sort_title" in details:
edits["titleSort.value"] = details["sort_title"]
edits["titleSort.locked"] = 1
if "content_rating" in details:
edits["contentRating.value"] = details["content_rating"]
edits["contentRating.locked"] = 1
if "summary" in details:
edits["summary.value"] = details["summary"]
edits["summary.locked"] = 1
if len(edits) > 0:
logger.debug(edits)
plex_collection.edit(**edits)
plex_collection.reload()
logger.info("Details: have been updated")
if "collection_mode" in details:
plex_collection.modeUpdate(mode=details["collection_mode"])
if "collection_order" in details:
plex_collection.sortUpdate(sort=details["collection_order"])
if library.asset_directory:
name_mapping = c
if "name_mapping" in collections[c]:
if collections[c]["name_mapping"]: name_mapping = collections[c]["name_mapping"]
else: logger.error("Collection Error: name_mapping attribute is blank")
path = os.path.join(library.asset_directory, "{}".format(name_mapping), "poster.*")
matches = glob.glob(path)
if len(matches) > 0:
for match in matches: posters_found.append(("file", os.path.abspath(match), "asset_directory"))
elif len(posters_found) == 0 and "poster" not in details: logger.warning("poster not found at: {}".format(os.path.abspath(path)))
path = os.path.join(library.asset_directory, "{}".format(name_mapping), "background.*")
matches = glob.glob(path)
if len(matches) > 0:
for match in matches: backgrounds_found.append(("file", os.path.abspath(match), "asset_directory"))
elif len(backgrounds_found) == 0 and "background" not in details: logger.warning("background not found at: {}".format(os.path.abspath(path)))
poster = util.choose_from_list(posters_found, "poster", list_type="tuple")
if not poster and "poster" in details: poster = details["poster"]
if poster:
if poster[0] == "url": plex_collection.uploadPoster(url=poster[1])
else: plex_collection.uploadPoster(filepath=poster[1])
logger.info("Detail: {} updated poster to [{}] {}".format(poster[2], poster[0], poster[1]))
background = util.choose_from_list(backgrounds_found, "background", list_type="tuple")
if not background and "background" in details: background = details["background"]
if background:
if background[0] == "url": plex_collection.uploadArt(url=background[1])
else: plex_collection.uploadArt(filepath=background[1])
logger.info("Detail: {} updated background to [{}] {}".format(background[2], background[0], background[1]))
except Exception as e:
util.print_stacktrace()
logger.error("Unknown Error: {}".format(e))
if library.show_unmanaged_collections is True:
logger.info("")
util.seperator("Unmanaged Collections in {} Library".format(library.name))
logger.info("")
unmanaged_count = 0
collections_in_plex = [str(pcol) for pcol in collections]
for col in library.get_all_collections():
if col.title not in collections_in_plex:
logger.info(col.title)
unmanaged_count += 1
logger.info("{} Unmanaged Collections".format(unmanaged_count))
else:
logger.error("No collection to update")
def map_guids(self, library):
movie_map = {}
show_map = {}
length = 0
count = 0
logger.info("Mapping {} Library: {}".format("Movie" if library.is_movie else "Show", library.name))
items = library.Plex.all()
for i, item in enumerate(items, 1):
length = util.print_return(length, "Processing: {}/{} {}".format(i, len(items), item.title))
id_type, main_id = self.get_id(item, library, length)
if id_type == "movie":
movie_map[main_id] = item.ratingKey
elif id_type == "show":
show_map[main_id] = item.ratingKey
util.print_end(length, "Processed {} {}".format(len(items), "Movies" if library.is_movie else "Shows"))
return movie_map, show_map
def get_id(self, item, library, length):
expired = None
tmdb_id = None
imdb_id = None
tvdb_id = None
anidb_id = None
mal_id = None
error_message = None
if self.Cache:
if library.is_movie: tmdb_id, expired = self.Cache.get_tmdb_id("movie", plex_guid=item.guid)
else: tvdb_id, expired = self.Cache.get_tvdb_id("show", plex_guid=item.guid)
if not tvdb_id and library.is_show:
tmdb_id, expired = self.Cache.get_tmdb_id("show", plex_guid=item.guid)
anidb_id, expired = self.Cache.get_anidb_id("show", plex_guid=item.guid)
if expired or (not tmdb_id and library.is_movie) or (not tvdb_id and not tmdb_id and library.is_show):
guid = requests.utils.urlparse(item.guid)
item_type = guid.scheme.split(".")[-1]
check_id = guid.netloc
if item_type == "plex" and library.is_movie:
for guid_tag in item.guids:
url_parsed = requests.utils.urlparse(guid_tag.id)
if url_parsed.scheme == "tmdb": tmdb_id = url_parsed.netloc
elif url_parsed.scheme == "imdb": imdb_id = url_parsed.netloc
elif item_type == "imdb": imdb_id = check_id
elif item_type == "thetvdb": tvdb_id = check_id
elif item_type == "themoviedb": tmdb_id = check_id
elif item_type == "hama":
if check_id.startswith("tvdb"): tvdb_id = re.search("-(.*)", check_id).group(1)
elif check_id.startswith("anidb"): anidb_id = re.search("-(.*)", check_id).group(1)
else: error_message = "Hama Agent ID: {} not supported".format(check_id)
elif item_type == "myanimelist": mal_id = check_id
elif item_type == "local": error_message = "No match in Plex"
else: error_message = "Agent {} not supported".format(item_type)
if not error_message:
if anidb_id and not tvdb_id:
try: tvdb_id = self.AniDB.convert_anidb_to_tvdb(anidb_id)
except Failed: pass
if anidb_id and not imdb_id:
try: imdb_id = self.AniDB.convert_anidb_to_imdb(anidb_id)
except Failed: pass
if mal_id:
try:
ids = self.MyAnimeListIDList.find_mal_ids(mal_id)
if "thetvdb_id" in ids and int(ids["thetvdb_id"]) > 0: tvdb_id = int(ids["thetvdb_id"])
elif "themoviedb_id" in ids and int(ids["themoviedb_id"]) > 0: tmdb_id = int(ids["themoviedb_id"])
else: raise Failed("MyAnimeList Error: MyAnimeList ID: {} has no other IDs associated with it".format(mal_id))
except Failed:
pass
if mal_id and not tvdb_id:
try: tvdb_id = self.MyAnimeListIDList.convert_mal_to_tvdb(mal_id)
except Failed: pass
if mal_id and not tmdb_id:
try: tmdb_id = self.MyAnimeListIDList.convert_mal_to_tmdb(mal_id)
except Failed: pass
if not tmdb_id and imdb_id and self.TMDb:
try: tmdb_id = self.TMDb.convert_imdb_to_tmdb(imdb_id)
except Failed: pass
if not tmdb_id and imdb_id and self.Trakt:
try: tmdb_id = self.Trakt.convert_imdb_to_tmdb(imdb_id)
except Failed: pass
if not tmdb_id and tvdb_id and self.TMDb:
try: tmdb_id = self.TMDb.convert_tvdb_to_tmdb(tvdb_id)
except Failed: pass
if not tmdb_id and tvdb_id and self.Trakt:
try: tmdb_id = self.Trakt.convert_tvdb_to_tmdb(tvdb_id)
except Failed: pass
if not imdb_id and tmdb_id and self.TMDb:
try: imdb_id = self.TMDb.convert_tmdb_to_imdb(tmdb_id)
except Failed: pass
if not imdb_id and tmdb_id and self.Trakt:
try: imdb_id = self.Trakt.convert_tmdb_to_imdb(tmdb_id)
except Failed: pass
if not imdb_id and tvdb_id and self.Trakt:
try: imdb_id = self.Trakt.convert_tmdb_to_imdb(tmdb_id)
except Failed: pass
if not tvdb_id and tmdb_id and self.TMDb and library.is_show:
try: tvdb_id = self.TMDb.convert_tmdb_to_tvdb(tmdb_id)
except Failed: pass
if not tvdb_id and tmdb_id and self.Trakt and library.is_show:
try: tvdb_id = self.Trakt.convert_tmdb_to_tvdb(tmdb_id)
except Failed: pass
if not tvdb_id and imdb_id and self.Trakt and library.is_show:
try: tvdb_id = self.Trakt.convert_imdb_to_tvdb(imdb_id)
except Failed: pass
if tvdb_id and not anidb_id:
try: anidb_id = self.AniDB.convert_tvdb_to_anidb(tvdb_id)
except Failed: pass
if imdb_id and not anidb_id:
try: anidb_id = self.AniDB.convert_imdb_to_anidb(imdb_id)
except Failed: pass
if tvdb_id and not mal_id:
try: mal_id = self.MyAnimeListIDList.convert_tvdb_to_mal(tvdb_id)
except Failed: pass
if tmdb_id and not mal_id:
try: mal_id = self.MyAnimeListIDList.convert_tmdb_to_mal(tmdb_id)
except Failed: pass
if (not tmdb_id and library.is_movie) or (not tvdb_id and not ((anidb_id or mal_id) and tmdb_id) and library.is_show):
service_name = "TMDb ID" if library.is_movie else "TVDb ID"
if self.TMDb and self.Trakt: api_name = "TMDb or Trakt"
elif self.TMDb: api_name = "TMDb"
elif self.Trakt: api_name = "Trakt"
else: api_name = None
if tmdb_id and imdb_id: id_name = "TMDb ID: {} or IMDb ID: {}".format(tmdb_id, imdb_id)
elif imdb_id and tvdb_id: id_name = "IMDb ID: {} or TVDb ID: {}".format(imdb_id, tvdb_id)
elif tmdb_id: id_name = "TMDb ID: {}".format(tmdb_id)
elif imdb_id: id_name = "IMDb ID: {}".format(imdb_id)
elif tvdb_id: id_name = "TVDb ID: {}".format(tvdb_id)
else: id_name = None
if anidb_id and not tmdb_id and not tvdb_id: error_message = "Unable to convert AniDb ID: {} to TMDb ID or TVDb ID".format(anidb_id)
elif mal_id and not tmdb_id and not tvdb_id: error_message = "Unable to convert MyAnimeList ID: {} to TMDb ID or TVDb ID".format(mal_id)
elif id_name and api_name: error_message = "Unable to convert {} to {} using {}".format(id_name, service_name, api_name)
elif id_name: error_message = "Configure TMDb or Trakt to covert {} to {}".format(id_name, service_name)
else: error_message = "No ID to convert to {}".format(service_name)
if self.Cache and (tmdb_id and library.is_movie) or ((tvdb_id or ((anidb_id or mal_id) and tmdb_id)) and library.is_show):
util.print_end(length, "Cache | {} | {:<46} | {:<6} | {:<10} | {:<6} | {:<5} | {:<5} | {}".format("^" if expired is True else "+", item.guid, tmdb_id if tmdb_id else "None", imdb_id if imdb_id else "None", tvdb_id if tvdb_id else "None", anidb_id if anidb_id else "None", mal_id if mal_id else "None", item.title))
self.Cache.update_guid("movie" if library.is_movie else "show", item.guid, tmdb_id, imdb_id, tvdb_id, anidb_id, mal_id, expired)
if tmdb_id and library.is_movie: return "movie", tmdb_id
elif tvdb_id and library.is_show: return "show", tvdb_id
elif (anidb_id or mal_id) and tmdb_id: return "movie", tmdb_id
else:
util.print_end(length, "{} {:<46} | {} for {}".format("Cache | ! |" if self.Cache else "Mapping Error:", item.guid, error_message, item.title))
return None, None