mirror of
https://github.com/meisnate12/Plex-Meta-Manager
synced 2024-11-10 06:54:21 +00:00
added templates #19
This commit is contained in:
parent
e11f19c04c
commit
be872dac65
5 changed files with 778 additions and 669 deletions
742
modules/builder.py
Normal file
742
modules/builder.py
Normal file
|
@ -0,0 +1,742 @@
|
|||
import glob, logging, os, re
|
||||
from datetime import datetime, timedelta
|
||||
from modules import util
|
||||
from modules.util import Failed
|
||||
|
||||
logger = logging.getLogger("Plex Meta Manager")
|
||||
|
||||
class CollectionBuilder:
|
||||
def __init__(self, config, library, name, data):
|
||||
self.config = config
|
||||
self.library = library
|
||||
self.name = name
|
||||
self.data = data
|
||||
self.details = {"arr_tag": None}
|
||||
self.methods = []
|
||||
self.filters = []
|
||||
self.posters = []
|
||||
self.backgrounds = []
|
||||
self.schedule = None
|
||||
|
||||
if "template" in data:
|
||||
if not self.library.templates:
|
||||
raise Failed("Collection Error: No templates found")
|
||||
elif not data["template"]:
|
||||
raise Failed("Collection Error: template attribute is blank")
|
||||
else:
|
||||
template_list = data["template"] if isinstance(data["template"], list) else [data["template"]]
|
||||
for data_template in template_list:
|
||||
if not isinstance(data_template, dict):
|
||||
raise Failed("Collection Error: template attribute is not a dictionary")
|
||||
elif "name" not in data_template:
|
||||
raise Failed("Collection Error: template sub-attribute name is required")
|
||||
elif not data_template["name"]:
|
||||
raise Failed("Collection Error: template sub-attribute name is blank")
|
||||
elif data_template["name"] not in self.library.templates:
|
||||
raise Failed("Collection Error: template {} not found".format(data_template["name"]))
|
||||
elif not isinstance(self.library.templates[data_template["name"]], dict):
|
||||
raise Failed("Collection Error: template {} is not a dictionary".format(data_template["name"]))
|
||||
else:
|
||||
for tm in data_template:
|
||||
if not data_template[tm]:
|
||||
raise Failed("Collection Error: template sub-attribute {} is blank".format(data_template[tm]))
|
||||
|
||||
template_name = data_template["name"]
|
||||
template = self.library.templates[template_name]
|
||||
default = {}
|
||||
if "default" in template:
|
||||
if template["default"]:
|
||||
if isinstance(template["default"], dict):
|
||||
for dv in template["default"]:
|
||||
if template["default"][dv]:
|
||||
default[dv] = template["default"][dv]
|
||||
else:
|
||||
raise Failed("Collection Error: template default sub-attribute {} is blank".format(dv))
|
||||
else:
|
||||
raise Failed("Collection Error: template sub-attribute default is not a dictionary")
|
||||
else:
|
||||
raise Failed("Collection Error: template sub-attribute default is blank")
|
||||
|
||||
|
||||
|
||||
for m in template:
|
||||
if m not in self.data and m != "default":
|
||||
if template[m]:
|
||||
attr = None
|
||||
def replace_txt(txt):
|
||||
txt = str(txt)
|
||||
for tm in data_template:
|
||||
if tm != "name" and "<<{}>>".format(tm) in txt:
|
||||
txt = txt.replace("<<{}>>".format(tm), str(data_template[tm]))
|
||||
if "<<collection_name>>" in txt:
|
||||
txt = txt.replace("<<collection_name>>", str(self.name))
|
||||
for dm in default:
|
||||
if "<<{}>>".format(dm) in txt:
|
||||
txt = txt.replace("<<{}>>".format(dm), str(default[dm]))
|
||||
if txt in ["true", "True"]: return True
|
||||
elif txt in ["false", "False"]: return False
|
||||
else:
|
||||
try: return int(txt)
|
||||
except ValueError: return txt
|
||||
if isinstance(template[m], dict):
|
||||
attr = {}
|
||||
for sm in template[m]:
|
||||
if isinstance(template[m][sm], list):
|
||||
temp_list = []
|
||||
for li in template[m][sm]:
|
||||
temp_list.append(replace_txt(li))
|
||||
attr[sm] = temp_list
|
||||
else:
|
||||
attr[sm] = replace_txt(template[m][sm])
|
||||
elif isinstance(template[m], list):
|
||||
attr = []
|
||||
for li in template[m]:
|
||||
if isinstance(li, dict):
|
||||
temp_dict = {}
|
||||
for sm in li:
|
||||
temp_dict[sm] = replace_txt(li[sm])
|
||||
attr.append(temp_dict)
|
||||
else:
|
||||
attr.append(replace_txt(li))
|
||||
else:
|
||||
attr = replace_txt(template[m])
|
||||
self.data[m] = attr
|
||||
else:
|
||||
raise Failed("Collection Error: template attribute {} is blank".format(m))
|
||||
|
||||
skip_collection = True
|
||||
if "schedule" not in data:
|
||||
skip_collection = False
|
||||
elif not data["schedule"]:
|
||||
logger.error("Collection Error: schedule attribute is blank. Running daily")
|
||||
skip_collection = False
|
||||
else:
|
||||
schedule_list = util.get_list(data["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
|
||||
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()]
|
||||
self.schedule += "\nScheduled weekly on {}".format(util.pretty_days[weekday])
|
||||
if weekday == current_time.weekday():
|
||||
skip_collection = False
|
||||
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:
|
||||
self.schedule += "\nScheduled 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
|
||||
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))
|
||||
self.schedule += "\nScheduled 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
|
||||
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 self.schedule is None:
|
||||
skip_collection = False
|
||||
if skip_collection:
|
||||
raise Failed("Skipping Collection {}".format(c))
|
||||
|
||||
logger.info("Scanning {} Collection".format(self.name))
|
||||
|
||||
self.collectionless = "plex_collectionless" in data
|
||||
|
||||
self.sync = self.library.sync_mode == "sync"
|
||||
if "sync_mode" in data:
|
||||
if not data["sync_mode"]: logger.warning("Collection Warning: sync_mode attribute is blank using general: {}".format(self.library.sync_mode))
|
||||
elif data["sync_mode"] not in ["append", "sync"]: logger.warning("Collection Warning: {} sync_mode invalid using general: {}".format(self.library.sync_mode, data["sync_mode"]))
|
||||
else: self.sync = data["sync_mode"] == "sync"
|
||||
|
||||
if "tmdb_person" in data:
|
||||
if data["tmdb_person"]:
|
||||
valid_names = []
|
||||
for tmdb_id in util.get_int_list(data["tmdb_person"], "TMDb Person ID"):
|
||||
person = config.TMDb.get_person(tmdb_id)
|
||||
valid_names.append(person.name)
|
||||
if "summary" not in self.details and hasattr(person, "biography") and person.biography:
|
||||
self.details["summary"] = person.biography
|
||||
if "poster" not in self.details and hasattr(person, "profile_path") and person.profile_path:
|
||||
self.details["poster"] = ("url", "{}{}".format(config.TMDb.image_url, person.profile_path), "tmdb_person")
|
||||
if len(valid_names) > 0: self.details["tmdb_person"] = valid_names
|
||||
else: raise Failed("Collection Error: No valid TMDb Person IDs in {}".format(data["tmdb_person"]))
|
||||
else:
|
||||
raise Failed("Collection Error: tmdb_person attribute is blank")
|
||||
|
||||
for m in data:
|
||||
if "tmdb" in m and not config.TMDb: raise Failed("Collection Error: {} requires TMDb to be configured".format(m))
|
||||
elif "trakt" in m and not config.Trakt: raise Failed("Collection Error: {} requires Trakt todo be configured".format(m))
|
||||
elif "imdb" in m and not config.IMDb: raise Failed("Collection Error: {} requires TMDb or Trakt to be configured".format(m))
|
||||
elif "tautulli" in m and not self.library.Tautulli: raise Failed("Collection Error: {} requires Tautulli to be configured".format(m))
|
||||
elif "mal" in m and not config.MyAnimeList: raise Failed("Collection Error: {} requires MyAnimeList to be configured".format(m))
|
||||
elif data[m] is not None:
|
||||
logger.debug("")
|
||||
logger.debug("Method: {}".format(m))
|
||||
logger.debug("Value: {}".format(data[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 self.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 self.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 self.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 self.collectionless:
|
||||
raise Failed("Collection Error: {} attribute does not work for Collectionless collection".format(method_name))
|
||||
elif method_name == "tmdb_summary":
|
||||
self.details["summary"] = config.TMDb.get_movie_show_or_collection(util.regex_first_int(data[m], "TMDb ID"), self.library.is_movie).overview
|
||||
elif method_name == "tmdb_description":
|
||||
self.details["summary"] = config.TMDb.get_list(util.regex_first_int(data[m], "TMDb List ID")).description
|
||||
elif method_name == "tmdb_biography":
|
||||
self.details["summary"] = config.TMDb.get_person(util.regex_first_int(data[m], "TMDb Person ID")).biography
|
||||
elif method_name == "collection_mode":
|
||||
if data[m] in ["default", "hide", "hide_items", "show_items", "hideItems", "showItems"]:
|
||||
if data[m] == "hide_items": self.details[method_name] = "hideItems"
|
||||
elif data[m] == "show_items": self.details[method_name] = "showItems"
|
||||
else: self.details[method_name] = data[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(data[m]))
|
||||
elif method_name == "collection_order":
|
||||
if data[m] in ["release", "alpha"]:
|
||||
self.details[method_name] = data[m]
|
||||
else:
|
||||
raise Failed("Collection Error: {} collection_order Invalid\n| \trelease (Order Collection by release dates)\n| \talpha (Order Collection Alphabetically)".format(data[m]))
|
||||
elif method_name == "url_poster":
|
||||
self.posters.append(("url", data[m], method_name))
|
||||
elif method_name == "tmdb_poster":
|
||||
self.posters.append(("url", "{}{}".format(config.TMDb.image_url, config.TMDb.get_movie_show_or_collection(util.regex_first_int(data[m], "TMDb ID"), self.library.is_movie).poster_path), method_name))
|
||||
elif method_name == "tmdb_profile":
|
||||
self.posters.append(("url", "{}{}".format(config.TMDb.image_url, config.TMDb.get_person(util.regex_first_int(data[m], "TMDb Person ID")).profile_path), method_name))
|
||||
elif method_name == "file_poster":
|
||||
if os.path.exists(data[m]): self.posters.append(("file", os.path.abspath(data[m]), method_name))
|
||||
else: raise Failed("Collection Error: Poster Path Does Not Exist: {}".format(os.path.abspath(data[m])))
|
||||
elif method_name == "url_background":
|
||||
self.backgrounds.append(("url", data[m], method_name))
|
||||
elif method_name == "tmdb_background":
|
||||
self.backgrounds.append(("url", "{}{}".format(config.TMDb.image_url, config.TMDb.get_movie_show_or_collection(util.regex_first_int(data[m], "TMDb ID"), self.library.is_movie).poster_path), method_name))
|
||||
elif method_name == "file_background":
|
||||
if os.path.exists(data[m]): self.backgrounds.append(("file", os.path.abspath(data[m]), method_name))
|
||||
else: raise Failed("Collection Error: Background Path Does Not Exist: {}".format(os.path.abspath(data[m])))
|
||||
elif method_name in ["add_to_arr", "show_filtered"]:
|
||||
if isinstance(data[m], bool): self.details[method_name] = data[m]
|
||||
else: raise Failed("Collection Error: {} must be either true or false".format(method_name))
|
||||
elif method_name == "arr_tag":
|
||||
self.details[method_name] = util.get_list(data[m])
|
||||
elif method_name in util.all_details:
|
||||
self.details[method_name] = data[m]
|
||||
elif method_name in ["year", "year.not"]:
|
||||
self.methods.append(("plex_search", [[(method_name, util.get_year_list(data[m], method_name))]]))
|
||||
elif method_name in ["decade", "decade.not"]:
|
||||
self.methods.append(("plex_search", [[(method_name, util.get_int_list(data[m], util.remove_not(method_name)))]]))
|
||||
elif method_name in util.tmdb_searches:
|
||||
final_values = []
|
||||
for value in util.get_list(data[m]):
|
||||
if value.lower() == "tmdb" and "tmdb_person" in self.details:
|
||||
for name in self.details["tmdb_person"]:
|
||||
final_values.append(name)
|
||||
else:
|
||||
final_values.append(value)
|
||||
self.methods.append(("plex_search", [[(method_name, final_values)]]))
|
||||
elif method_name in util.plex_searches:
|
||||
self.methods.append(("plex_search", [[(method_name, util.get_list(data[m]))]]))
|
||||
elif method_name == "plex_all":
|
||||
self.methods.append((method_name, [""]))
|
||||
elif method_name == "plex_collection":
|
||||
self.methods.append((method_name, self.library.validate_collections(data[m] if isinstance(data[m], list) else [data[m]])))
|
||||
elif method_name == "anidb_popular":
|
||||
list_count = util.regex_first_int(data[m], "List Size", default=40)
|
||||
if 1 <= list_count <= 30:
|
||||
self.methods.append((method_name, [list_count]))
|
||||
else:
|
||||
logger.warning("Collection Error: anidb_popular must be an integer between 1 and 30 defaulting to 30")
|
||||
self.methods.append((method_name, [30]))
|
||||
elif method_name == "mal_id":
|
||||
self.methods.append((method_name, util.get_int_list(data[m], "MyAnimeList ID")))
|
||||
elif method_name in ["anidb_id", "anidb_relation"]:
|
||||
self.methods.append((method_name, config.AniDB.validate_anidb_list(util.get_int_list(data[m], "AniDB ID"), self.library.Plex.language)))
|
||||
elif method_name == "trakt_list":
|
||||
self.methods.append((method_name, config.Trakt.validate_trakt_list(util.get_list(data[m]))))
|
||||
elif method_name == "trakt_watchlist":
|
||||
self.methods.append((method_name, config.Trakt.validate_trakt_watchlist(util.get_list(data[m]), self.library.is_movie)))
|
||||
elif method_name == "imdb_list":
|
||||
new_list = []
|
||||
for imdb_list in util.get_list(data[m], split=False):
|
||||
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: imdb_list attribute url is required")
|
||||
list_count = util.regex_first_int(imdb_list["limit"], "List Limit", default=0) if "limit" in imdb_list and imdb_list["limit"] else 0
|
||||
else:
|
||||
imdb_url = str(imdb_list)
|
||||
list_count = 0
|
||||
new_list.append({"url": imdb_url, "limit": list_count})
|
||||
self.methods.append((method_name, new_list))
|
||||
elif method_name in util.dictionary_lists:
|
||||
if isinstance(data[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 f in data[m]:
|
||||
if f in util.method_alias or (f.endswith(".not") and f[:-4] in util.method_alias):
|
||||
filter = (util.method_alias[f[:-4]] + f[-4:]) if f.endswith(".not") else util.method_alias[f]
|
||||
logger.warning("Collection Warning: {} filter will run as {}".format(f, filter))
|
||||
else:
|
||||
filter = f
|
||||
if filter in util.movie_only_filters and self.library.is_show: raise Failed("Collection Error: {} filter only works for movie libraries".format(filter))
|
||||
elif data[m][f] is None: raise Failed("Collection Error: {} filter is blank".format(filter))
|
||||
elif filter in util.all_filters: self.filters.append((filter, data[m][f]))
|
||||
else: raise Failed("Collection Error: {} filter not supported".format(filter))
|
||||
elif method_name == "plex_collectionless":
|
||||
new_dictionary = {}
|
||||
prefix_list = []
|
||||
if "exclude_prefix" in data[m] and data[m]["exclude_prefix"]:
|
||||
if isinstance(data[m]["exclude_prefix"], list): prefix_list.extend(data[m]["exclude_prefix"])
|
||||
else: prefix_list.append(str(data[m]["exclude_prefix"]))
|
||||
exact_list = []
|
||||
if "exclude" in data[m] and data[m]["exclude"]:
|
||||
if isinstance(data[m]["exclude"], list): exact_list.extend(data[m]["exclude"])
|
||||
else: exact_list.append(str(data[m]["exclude"]))
|
||||
if len(prefix_list) == 0 and len(exact_list) == 0: raise Failed("Collection Error: you must have at least one exclusion")
|
||||
self.details["add_to_arr"] = False
|
||||
self.details["collection_mode"] = "hide"
|
||||
self.sync = True
|
||||
new_dictionary["exclude_prefix"] = prefix_list
|
||||
new_dictionary["exclude"] = exact_list
|
||||
self.methods.append((method_name, [new_dictionary]))
|
||||
elif method_name == "plex_search":
|
||||
searches = []
|
||||
used = []
|
||||
for s in data[m]:
|
||||
if s in util.method_alias or (s.endswith(".not") and s[:-4] in util.method_alias):
|
||||
search = (util.method_alias[s[:-4]] + s[-4:]) if s.endswith(".not") else util.method_alias[s]
|
||||
logger.warning("Collection Warning: {} plex search attribute will run as {}".format(s, search))
|
||||
else:
|
||||
search = s
|
||||
if search in util.movie_only_searches and self.library.is_show:
|
||||
raise Failed("Collection Error: {} plex search attribute only works for movie libraries".format(search))
|
||||
elif util.remove_not(search) in used:
|
||||
raise Failed("Collection Error: Only one instance of {} can be used try using it as a filter instead".format(search))
|
||||
elif search in ["year", "year.not"]:
|
||||
years = util.get_year_list(data[m][s], search)
|
||||
if len(years) > 0:
|
||||
used.append(util.remove_not(search))
|
||||
searches.append((search, util.get_int_list(data[m][s], util.remove_not(search))))
|
||||
elif search in util.plex_searches:
|
||||
used.append(util.remove_not(search))
|
||||
searches.append((search, util.get_list(data[m][s])))
|
||||
else:
|
||||
logger.error("Collection Error: {} plex search attribute not supported".format(search))
|
||||
self.methods.append((method_name, [searches]))
|
||||
elif method_name == "tmdb_discover":
|
||||
new_dictionary = {"limit": 100}
|
||||
for attr in data[m]:
|
||||
if data[m][attr]:
|
||||
attr_data = data[m][attr]
|
||||
if (self.library.is_movie and attr in util.discover_movie) or (self.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:
|
||||
raise Failed("Collection Error: {} 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:
|
||||
raise Failed("Collection Error: {} attribute {}: {} must match pattern ^[A-Z]{2}$ e.g. US".format(m, attr, attr_data))
|
||||
elif attr == "sort_by":
|
||||
if (self.library.is_movie and attr_data in util.discover_movie_sort) or (self.library.is_show and attr_data in util.discover_tv_sort):
|
||||
new_dictionary[attr] = attr_data
|
||||
else:
|
||||
raise Failed("Collection Error: {} attribute {}: {} is invalid".format(m, attr, attr_data))
|
||||
elif attr == "certification_country":
|
||||
if "certification" in data[m] or "certification.lte" in data[m] or "certification.gte" in data[m]:
|
||||
new_dictionary[attr] = attr_data
|
||||
else:
|
||||
raise Failed("Collection Error: {} 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 data[m]:
|
||||
new_dictionary[attr] = attr_data
|
||||
else:
|
||||
raise Failed("Collection Error: {} 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:
|
||||
raise Failed("Collection Error: {} 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:
|
||||
raise Failed("Collection Error: {} 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:
|
||||
raise Failed("Collection Error: {} 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:
|
||||
raise Failed("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:
|
||||
raise Failed("Collection Error: {} attribute {}: must be a valid number greater then 0".format(m, attr))
|
||||
else:
|
||||
raise Failed("Collection Error: {} attribute {} not supported".format(m, attr))
|
||||
else:
|
||||
raise Failed("Collection Error: {} parameter {} is blank".format(m, attr))
|
||||
if len(new_dictionary) > 1:
|
||||
self.methods.append((method_name, [new_dictionary]))
|
||||
else:
|
||||
raise Failed("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", data[m], 30)
|
||||
new_dictionary["list_size"] = get_int(method_name, "list_size", data[m], 10)
|
||||
new_dictionary["list_buffer"] = get_int(method_name, "list_buffer", data[m], 20)
|
||||
self.methods.append((method_name, [new_dictionary]))
|
||||
elif method_name == "mal_season":
|
||||
new_dictionary = {"sort_by": "anime_num_list_users"}
|
||||
if "sort_by" not in data[m]: logger.warning("Collection Warning: mal_season sort_by attribute not found using members as default")
|
||||
elif not data[m]["sort_by"]: logger.warning("Collection Warning: mal_season sort_by attribute is blank using members as default")
|
||||
elif data[m]["sort_by"] not in util.mal_season_sort: logger.warning("Collection Warning: mal_season sort_by attribute {} invalid must be either 'members' or 'score' using members as default".format(data[m]["sort_by"]))
|
||||
else: new_dictionary["sort_by"] = util.mal_season_sort[data[m]["sort_by"]]
|
||||
|
||||
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"
|
||||
|
||||
if "season" not in data[m]: logger.warning("Collection Warning: mal_season season attribute not found using the current season: {} as default".format(new_dictionary["season"]))
|
||||
elif not data[m]["season"]: logger.warning("Collection Warning: mal_season season attribute is blank using the current season: {} as default".format(new_dictionary["season"]))
|
||||
elif data[m]["season"] not in util.pretty_seasons: logger.warning("Collection Warning: mal_season season attribute {} invalid must be either 'winter', 'spring', 'summer' or 'fall' using the current season: {} as default".format(data[m]["season"], new_dictionary["season"]))
|
||||
else: new_dictionary["season"] = data[m]["season"]
|
||||
|
||||
new_dictionary["year"] = get_int(method_name, "year", data[m], current_time.year, min=1917, max=current_time.year + 1)
|
||||
new_dictionary["limit"] = get_int(method_name, "limit", data[m], 100, max=500)
|
||||
self.methods.append((method_name, [new_dictionary]))
|
||||
elif method_name == "mal_userlist":
|
||||
new_dictionary = {"status": "all", "sort_by": "list_score"}
|
||||
if "username" not in data[m]: raise Failed("Collection Error: mal_userlist username attribute is required")
|
||||
elif not data[m]["username"]: raise Failed("Collection Error: mal_userlist username attribute is blank")
|
||||
else: new_dictionary["username"] = data[m]["username"]
|
||||
|
||||
if "status" not in data[m]: logger.warning("Collection Warning: mal_season status attribute not found using all as default")
|
||||
elif not data[m]["status"]: logger.warning("Collection Warning: mal_season status attribute is blank using all as default")
|
||||
elif data[m]["status"] not in util.mal_userlist_status: logger.warning("Collection Warning: mal_season status attribute {} invalid must be either 'all', 'watching', 'completed', 'on_hold', 'dropped' or 'plan_to_watch' using all as default".format(data[m]["status"]))
|
||||
else: new_dictionary["status"] = util.mal_userlist_status[data[m]["status"]]
|
||||
|
||||
if "sort_by" not in data[m]: logger.warning("Collection Warning: mal_season sort_by attribute not found using score as default")
|
||||
elif not data[m]["sort_by"]: logger.warning("Collection Warning: mal_season sort_by attribute is blank using score as default")
|
||||
elif data[m]["sort_by"] not in util.mal_userlist_sort: logger.warning("Collection Warning: mal_season sort_by attribute {} invalid must be either 'score', 'last_updated', 'title' or 'start_date' using score as default".format(data[m]["sort_by"]))
|
||||
else: new_dictionary["sort_by"] = util.mal_userlist_sort[data[m]["sort_by"]]
|
||||
|
||||
new_dictionary["limit"] = get_int(method_name, "limit", data[m], 100, max=1000)
|
||||
self.methods.append((method_name, [new_dictionary]))
|
||||
else:
|
||||
raise Failed("Collection Error: {} attribute is not a dictionary: {}".format(m, data[m]))
|
||||
elif method_name in util.count_lists:
|
||||
list_count = util.regex_first_int(data[m], "List Size", default=20)
|
||||
if list_count < 1:
|
||||
logger.warning("Collection Warning: {} must be an integer greater then 0 defaulting to 20".format(method_name))
|
||||
list_count = 20
|
||||
self.methods.append((method_name, [list_count]))
|
||||
elif method_name in util.tmdb_lists:
|
||||
values = config.TMDb.validate_tmdb_list(util.get_int_list(data[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 = config.TMDb.get_movie_show_or_collection(values[0], self.library.is_movie)
|
||||
if "summary" not in self.details and hasattr(item, "overview") and item.overview:
|
||||
self.details["summary"] = item.overview
|
||||
if "background" not in self.details and hasattr(item, "backdrop_path") and item.backdrop_path:
|
||||
self.details["background"] = ("url", "{}{}".format(config.TMDb.image_url, item.backdrop_path), method_name[:-8])
|
||||
if "poster" not in self.details and hasattr(item, "poster_path") and item.poster_path:
|
||||
self.details["poster"] = ("url", "{}{}".format(config.TMDb.image_url, item.poster_path), method_name[:-8])
|
||||
else:
|
||||
item = config.TMDb.get_list(values[0])
|
||||
if "summary" not in self.details and hasattr(item, "description") and item.description:
|
||||
self.details["summary"] = item.description
|
||||
self.methods.append((method_name[:-8], values))
|
||||
else:
|
||||
self.methods.append((method_name, values))
|
||||
elif method_name in util.all_lists:
|
||||
self.methods.append((method_name, util.get_list(data[m])))
|
||||
elif method_name not in util.other_attributes:
|
||||
raise Failed("Collection Error: {} attribute not supported".format(method_name))
|
||||
else:
|
||||
raise Failed("Collection Error: {} attribute is blank".format(m))
|
||||
|
||||
self.do_arr = False
|
||||
if self.library.Radarr:
|
||||
self.do_arr = self.details["add_to_arr"] if "add_to_arr" in self.details else self.library.Radarr.add
|
||||
if self.library.Sonarr:
|
||||
self.do_arr = self.details["add_to_arr"] if "add_to_arr" in self.details else self.library.Sonarr.add
|
||||
|
||||
def run_methods(self, collection_obj, collection_name, map, movie_map, show_map):
|
||||
items_found = 0
|
||||
for method, values in self.methods:
|
||||
logger.debug("")
|
||||
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:
|
||||
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("")
|
||||
logger.debug("Value: {}".format(value))
|
||||
if method == "plex_all":
|
||||
logger.info("Processing {} {}".format(pretty, "Movies" if self.library.is_movie else "Shows"))
|
||||
items = self.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 self.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 = self.library.Plex.search(**search_terms)
|
||||
items_found += len(items)
|
||||
elif method == "plex_collectionless":
|
||||
good_collections = []
|
||||
for col in self.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 = self.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 self.library.is_movie else "Shows"))
|
||||
elif "tautulli" in method:
|
||||
items = self.library.Tautulli.get_items(self.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.config.AniDB.get_items(method, value, self.library.Plex.language))
|
||||
elif "mal" in method: items_found += check_map(self.config.MyAnimeList.get_items(method, value))
|
||||
elif "tvdb" in method: items_found += check_map(self.config.TVDb.get_items(method, value, self.library.Plex.language))
|
||||
elif "imdb" in method: items_found += check_map(self.config.IMDb.get_items(method, value, self.library.Plex.language))
|
||||
elif "tmdb" in method: items_found += check_map(self.config.TMDb.get_items(method, value, self.library.is_movie))
|
||||
elif "trakt" in method: items_found += check_map(self.config.Trakt.get_items(method, value, self.library.is_movie))
|
||||
else: logger.error("Collection Error: {} method not supported".format(method))
|
||||
|
||||
if len(items) > 0: map = self.library.add_to_collection(collection_obj if collection_obj else collection_name, items, self.filters, self.library.show_filtered, map, movie_map, show_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:
|
||||
not_lang = None
|
||||
terms = None
|
||||
for filter_method, filter_data in self.filters:
|
||||
if filter_method.startswith("original_language"):
|
||||
terms = util.get_list(filter_data, lower=True)
|
||||
not_lang = filter_method.endswith(".not")
|
||||
break
|
||||
|
||||
missing_movies_with_names = []
|
||||
for missing_id in missing_movies:
|
||||
try:
|
||||
movie = self.config.TMDb.get_movie(missing_id)
|
||||
title = str(movie.title)
|
||||
if not_lang is None or (not_lang is True and movie.original_language not in terms) or (not_lang is False and movie.original_language in terms):
|
||||
missing_movies_with_names.append((title, missing_id))
|
||||
logger.info("{} Collection | ? | {} (TMDb: {})".format(collection_name, title, missing_id))
|
||||
elif self.library.show_filtered is True:
|
||||
logger.info("{} Collection | X | {} (TMDb: {})".format(collection_name, title, missing_id))
|
||||
except Failed as e:
|
||||
logger.error(e)
|
||||
logger.info("{} Movie{} Missing".format(len(missing_movies_with_names), "s" if len(missing_movies_with_names) > 1 else ""))
|
||||
self.library.add_missing(collection_name, missing_movies_with_names, True)
|
||||
if self.do_arr and self.library.Radarr:
|
||||
self.library.Radarr.add_tmdb([missing_id for title, missing_id in missing_movies_with_names], tag=self.details["arr_tag"])
|
||||
if len(missing_shows) > 0 and self.library.is_show:
|
||||
missing_shows_with_names = []
|
||||
for missing_id in missing_shows:
|
||||
try:
|
||||
title = str(self.config.TVDb.get_series(self.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_with_names), "s" if len(missing_shows_with_names) > 1 else ""))
|
||||
self.library.add_missing(c, missing_shows_with_names, False)
|
||||
if self.do_arr and self.library.Sonarr:
|
||||
self.library.Sonarr.add_tvdb([missing_id for title, missing_id in missing_shows_with_names], tag=self.details["arr_tag"])
|
||||
|
||||
if self.sync 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 self.library.is_movie else "Show", "s" if count_removed == 1 else ""))
|
||||
logger.info("")
|
||||
|
||||
def update_details(self, collection):
|
||||
edits = {}
|
||||
if "sort_title" in self.details:
|
||||
edits["titleSort.value"] = self.details["sort_title"]
|
||||
edits["titleSort.locked"] = 1
|
||||
if "content_rating" in self.details:
|
||||
edits["contentRating.value"] = self.details["content_rating"]
|
||||
edits["contentRating.locked"] = 1
|
||||
if "summary" in self.details:
|
||||
edits["summary.value"] = self.details["summary"]
|
||||
edits["summary.locked"] = 1
|
||||
if len(edits) > 0:
|
||||
logger.debug(edits)
|
||||
collection.edit(**edits)
|
||||
collection.reload()
|
||||
logger.info("Details: have been updated")
|
||||
if "collection_mode" in self.details:
|
||||
collection.modeUpdate(mode=self.details["collection_mode"])
|
||||
if "collection_order" in self.details:
|
||||
collection.sortUpdate(sort=self.details["collection_order"])
|
||||
|
||||
if self.library.asset_directory:
|
||||
name_mapping = self.name
|
||||
if "name_mapping" in self.details:
|
||||
if self.details["name_mapping"]: name_mapping = self.details["name_mapping"]
|
||||
else: logger.error("Collection Error: name_mapping attribute is blank")
|
||||
path = os.path.join(self.library.asset_directory, "{}".format(name_mapping), "poster.*")
|
||||
matches = glob.glob(path)
|
||||
if len(matches) > 0:
|
||||
for match in matches:
|
||||
self.posters.append(("file", os.path.abspath(match), "asset_directory"))
|
||||
elif len(self.posters) == 0 and "poster" not in self.details:
|
||||
logger.warning("poster not found at: {}".format(os.path.abspath(path)))
|
||||
path = os.path.join(self.library.asset_directory, "{}".format(name_mapping), "background.*")
|
||||
matches = glob.glob(path)
|
||||
if len(matches) > 0:
|
||||
for match in matches:
|
||||
self.backgrounds.append(("file", os.path.abspath(match), "asset_directory"))
|
||||
elif len(self.backgrounds) == 0 and "background" not in self.details:
|
||||
logger.warning("background not found at: {}".format(os.path.abspath(path)))
|
||||
|
||||
poster = util.choose_from_list(self.posters, "poster", list_type="tuple")
|
||||
if not poster and "poster" in self.details: poster = self.details["poster"]
|
||||
if poster:
|
||||
if poster[0] == "url": collection.uploadPoster(url=poster[1])
|
||||
else: collection.uploadPoster(filepath=poster[1])
|
||||
logger.info("Detail: {} updated poster to [{}] {}".format(poster[2], poster[0], poster[1]))
|
||||
|
||||
background = util.choose_from_list(self.backgrounds, "background", list_type="tuple")
|
||||
if not background and "background" in self.details: background = self.details["background"]
|
||||
if background:
|
||||
if background[0] == "url": collection.uploadArt(url=background[1])
|
||||
else: collection.uploadArt(filepath=background[1])
|
||||
logger.info("Detail: {} updated background to [{}] {}".format(background[2], background[0], background[1]))
|
||||
|
||||
if self.library.asset_directory:
|
||||
path = os.path.join(self.library.asset_directory, "{}".format(name_mapping))
|
||||
if os.path.isdir(path):
|
||||
dirs = [folder for folder in os.listdir(path) if os.path.isdir(os.path.join(path, folder))]
|
||||
if len(dirs) > 0:
|
||||
for item in collection.items():
|
||||
folder = os.path.basename(os.path.dirname(item.locations[0]))
|
||||
if folder in dirs:
|
||||
files = [file for file in os.listdir(os.path.join(path, folder)) if os.path.isfile(os.path.join(path, folder, file))]
|
||||
poster_path = None
|
||||
background_path = None
|
||||
for file in files:
|
||||
if poster_path is None and file.startswith("poster."):
|
||||
poster_path = os.path.join(path, folder, file)
|
||||
if background_path is None and file.startswith("background."):
|
||||
background_path = os.path.join(path, folder, file)
|
||||
if poster_path:
|
||||
item.uploadPoster(filepath=poster_path)
|
||||
logger.info("Detail: asset_directory updated {}'s poster to [file] {}".format(item.title, poster_path))
|
||||
if background_path:
|
||||
item.uploadArt(filepath=background_path)
|
||||
logger.info("Detail: asset_directory updated {}'s background to [file] {}".format(item.title, background_path))
|
||||
if poster_path is None and background_path is None:
|
||||
logger.warning("No Files Found: {}".format(os.path.join(path, folder)))
|
||||
else:
|
||||
logger.warning("No Folder: {}".format(os.path.join(path, folder)))
|
|
@ -1,7 +1,7 @@
|
|||
import glob, json, logging, os, re, requests
|
||||
from datetime import datetime, timedelta
|
||||
import glob, logging, os, re, requests
|
||||
from modules import util
|
||||
from modules.anidb import AniDBAPI
|
||||
from modules.builder import CollectionBuilder
|
||||
from modules.cache import Cache
|
||||
from modules.imdb import IMDbAPI
|
||||
from modules.plex import PlexAPI
|
||||
|
@ -44,7 +44,9 @@ class Config:
|
|||
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 not data[attribute] and data[attribute] != False:
|
||||
if default_is_none is True: return None
|
||||
else: 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)
|
||||
|
@ -138,6 +140,10 @@ class Config:
|
|||
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)
|
||||
|
||||
util.seperator()
|
||||
|
||||
logger.info("Connecting to Plex Libraries...")
|
||||
|
||||
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
|
||||
|
@ -170,9 +176,6 @@ class Config:
|
|||
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)
|
||||
|
@ -449,71 +452,11 @@ class Config:
|
|||
logger.info("")
|
||||
|
||||
map = {}
|
||||
details = {}
|
||||
methods = []
|
||||
filters = []
|
||||
posters_found = []
|
||||
backgrounds_found = []
|
||||
collectionless = "plex_collectionless" in collections[c]
|
||||
skip_collection = True
|
||||
show_filtered = library.show_filtered
|
||||
|
||||
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))
|
||||
try:
|
||||
builder = CollectionBuilder(self, library, c, collections[c])
|
||||
except Exception as e:
|
||||
util.print_stacktrace()
|
||||
logger.error(e)
|
||||
continue
|
||||
|
||||
try:
|
||||
|
@ -523,12 +466,11 @@ class Config:
|
|||
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:
|
||||
if builder.schedule is not None:
|
||||
print_multiline(builder.schedule, info=True)
|
||||
|
||||
logger.info("")
|
||||
if builder.sync:
|
||||
logger.info("Sync Mode: sync")
|
||||
if collection_obj:
|
||||
for item in collection_obj.items():
|
||||
|
@ -536,501 +478,12 @@ class Config:
|
|||
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("")
|
||||
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 == "arr_tag": details[method_name] = util.get_list(collections[c][m])
|
||||
elif method_name == "show_filtered":
|
||||
if isinstance(collections[c][m], bool): show_filtered = collections[c][m]
|
||||
else: raise Failed("Collection Error: show_filtered must be either true or false using the default 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], split=False):
|
||||
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: imdb_list attribute url is required")
|
||||
list_count = util.regex_first_int(imdb_list["limit"], "List Limit", default=0) if "limit" in imdb_list and imdb_list["limit"] else 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 collections[c][m][filter] is None:
|
||||
logger.error("Collection Error: {} filter is blank".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(final_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:
|
||||
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 Warning: mal_season sort_by attribute not found using members as default")
|
||||
elif not collections[c][m]["sort_by"]: logger.warning("Collection Warning: 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 Warning: 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 Warning: 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 Warning: 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 Warning: 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 Warning: mal_season status attribute not found using all as default")
|
||||
elif not collections[c][m]["status"]: logger.warning("Collection Warning: mal_season status attribute is blank using all as default")
|
||||
elif collections[c][m]["status"] not in util.mal_userlist_status: logger.warning("Collection Warning: 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 Warning: mal_season sort_by attribute not found using score as default")
|
||||
elif not collections[c][m]["sort_by"]: logger.warning("Collection Warning: 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 Warning: 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 util.other_attributes: 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):
|
||||
for i, f in enumerate(builder.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
|
||||
|
||||
movie_tag = details["arr_tag"] if "arr_tag" in details else None
|
||||
show_tag = details["arr_tag"] if "arr_tag" in details else None
|
||||
|
||||
items_found = 0
|
||||
|
||||
for method, values in methods:
|
||||
logger.debug("")
|
||||
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:
|
||||
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("")
|
||||
logger.debug("Value: {}".format(value))
|
||||
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, show_filtered, map, movie_map, show_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:
|
||||
not_lang = None
|
||||
terms = None
|
||||
for filter_method, filter_data in filters:
|
||||
if filter_method.startswith("original_language"):
|
||||
terms = util.get_list(filter_data, lower=True)
|
||||
not_lang = filter_method.endswith(".not")
|
||||
break
|
||||
|
||||
missing_movies_with_names = []
|
||||
for missing_id in missing_movies:
|
||||
try:
|
||||
movie = self.TMDb.get_movie(missing_id)
|
||||
title = str(movie.title)
|
||||
if not_lang is None or (not_lang is True and movie.original_language not in terms) or (not_lang is False and movie.original_language in terms):
|
||||
missing_movies_with_names.append((title, missing_id))
|
||||
logger.info("{} Collection | ? | {} (TMDb: {})".format(collection_name, title, missing_id))
|
||||
elif show_filtered is True:
|
||||
logger.info("{} Collection | X | {} (TMDb: {})".format(collection_name, title, missing_id))
|
||||
except Failed as e:
|
||||
logger.error(e)
|
||||
logger.info("{} Movie{} Missing".format(len(missing_movies_with_names), "s" if len(missing_movies_with_names) > 1 else ""))
|
||||
library.add_missing(collection_name, missing_movies_with_names, True)
|
||||
if do_arr and library.Radarr:
|
||||
library.Radarr.add_tmdb([missing_id for title, missing_id in missing_movies_with_names], tag=movie_tag)
|
||||
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_with_names), "s" if len(missing_shows_with_names) > 1 else ""))
|
||||
library.add_missing(c, missing_shows_with_names, False)
|
||||
if do_arr and library.Sonarr:
|
||||
library.Sonarr.add_tvdb([missing_id for title, missing_id in missing_shows_with_names], tag=show_tag)
|
||||
|
||||
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("")
|
||||
builder.run_methods(collection_obj, collection_name, map, movie_map, show_map)
|
||||
|
||||
try:
|
||||
plex_collection = library.get_collection(collection_name)
|
||||
|
@ -1038,82 +491,8 @@ class Config:
|
|||
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"])
|
||||
builder.update_details(plex_collection)
|
||||
|
||||
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]))
|
||||
|
||||
if library.asset_directory:
|
||||
path = os.path.join(library.asset_directory, "{}".format(name_mapping))
|
||||
if os.path.isdir(path):
|
||||
dirs = [folder for folder in os.listdir(path) if os.path.isdir(os.path.join(path, folder))]
|
||||
if len(dirs) > 0:
|
||||
for item in plex_collection.items():
|
||||
folder = os.path.basename(os.path.dirname(item.locations[0]))
|
||||
if folder in dirs:
|
||||
files = [file for file in os.listdir(os.path.join(path, folder)) if os.path.isfile(os.path.join(path, folder, file))]
|
||||
poster_path = None
|
||||
background_path = None
|
||||
for file in files:
|
||||
if poster_path is None and file.startswith("poster."):
|
||||
poster_path = os.path.join(path, folder, file)
|
||||
if background_path is None and file.startswith("background."):
|
||||
background_path = os.path.join(path, folder, file)
|
||||
if poster_path:
|
||||
item.uploadPoster(filepath=poster_path)
|
||||
logger.info("Detail: asset_directory updated {}'s poster to [file] {}".format(item.title, poster_path))
|
||||
if background_path:
|
||||
item.uploadArt(filepath=background_path)
|
||||
logger.info("Detail: asset_directory updated {}'s background to [file] {}".format(item.title, background_path))
|
||||
if poster_path is None and background_path is None:
|
||||
logger.warning("No Files Found: {}".format(os.path.join(path, folder)))
|
||||
else:
|
||||
logger.warning("No Folder: {}".format(os.path.join(path, folder)))
|
||||
except Exception as e:
|
||||
util.print_stacktrace()
|
||||
logger.error("Unknown Error: {}".format(e))
|
||||
|
|
|
@ -29,17 +29,17 @@ class PlexAPI:
|
|||
try: self.data, ind, bsi = yaml.util.load_yaml_guess_indent(open(params["metadata_path"], encoding="utf-8"))
|
||||
except yaml.scanner.ScannerError as e: raise Failed("YAML Error: {}".format(str(e).replace("\n", "\n|\t ")))
|
||||
|
||||
self.metadata = None
|
||||
if "metadata" in self.data:
|
||||
if self.data["metadata"]: self.metadata = self.data["metadata"]
|
||||
else: logger.warning("Config Warning: metadata attribute is blank")
|
||||
else: logger.warning("Config Warning: metadata attribute not found")
|
||||
def get_dict(attribute):
|
||||
if attribute in self.data:
|
||||
if self.data[attribute]:
|
||||
if isinstance(self.data[attribute], dict): return self.data[attribute]
|
||||
else: logger.waring("Config Warning: {} must be a dictionary".format(attribute))
|
||||
else: logger.warning("Config Warning: {} attribute is blank".format(attribute))
|
||||
return None
|
||||
|
||||
self.collections = None
|
||||
if "collections" in self.data:
|
||||
if self.data["collections"]: self.collections = self.data["collections"]
|
||||
else: logger.warning("Config Warning: collections attribute is blank")
|
||||
else: logger.warning("Config Warning: collections attribute not found")
|
||||
self.metadata = get_dict("metadata")
|
||||
self.templates = get_dict("templates")
|
||||
self.collections = get_dict("collections")
|
||||
|
||||
if self.metadata is None and self.collections is None:
|
||||
raise Failed("YAML Error: metadata attributes or collections attribute required")
|
||||
|
@ -116,20 +116,6 @@ class PlexAPI:
|
|||
raise Failed("Collection Error: No valid Plex Collections in {}".format(collections[c][m]))
|
||||
return valid_collections
|
||||
|
||||
def del_collection_if_empty(self, collection):
|
||||
missing_data = {}
|
||||
if not os.path.exists(self.missing_path):
|
||||
with open(self.missing_path, "w"): pass
|
||||
try:
|
||||
missing_data, ind, bsi = yaml.util.load_yaml_guess_indent(open(self.missing_path))
|
||||
if not missing_data:
|
||||
missing_data = {}
|
||||
if collection in missing_data and len(missing_data[collection]) == 0:
|
||||
del missing_data[collection]
|
||||
yaml.round_trip_dump(missing_data, open(self.missing_path, "w"), indent=ind, block_seq_indent=bsi)
|
||||
except yaml.scanner.ScannerError as e:
|
||||
logger.error("YAML Error: {}".format(str(e).replace("\n", "\n|\t ")))
|
||||
|
||||
def add_missing(self, collection, items, is_movie):
|
||||
col_name = collection.encode("ascii", "replace").decode()
|
||||
if col_name not in self.missing:
|
||||
|
@ -152,7 +138,8 @@ class PlexAPI:
|
|||
max_length = len(str(total))
|
||||
length = 0
|
||||
for i, item in enumerate(items, 1):
|
||||
try: current = self.fetchItem(item.ratingKey if isinstance(item, (Movie, Show)) else int(item))
|
||||
try:
|
||||
current = self.fetchItem(item.ratingKey if isinstance(item, (Movie, Show)) else int(item))
|
||||
except (BadRequest, NotFound):
|
||||
logger.error("Plex Error: Item {} not found".format(item))
|
||||
continue
|
||||
|
|
|
@ -89,7 +89,7 @@ class TraktAPI:
|
|||
lookup = Trakt["search"].lookup(external_id, from_source, media_type)
|
||||
if lookup:
|
||||
lookup = lookup[0] if isinstance(lookup, list) else lookup
|
||||
if lookup.get_key(to_source)):
|
||||
if lookup.get_key(to_source):
|
||||
return lookup.get_key(to_source) if to_source == "imdb" else int(lookup.get_key(to_source))
|
||||
raise Failed("No {} ID found for {} ID {}".format(to_source.upper().replace("B", "b"), from_source.upper().replace("B", "b"), external_id))
|
||||
|
||||
|
|
|
@ -254,6 +254,7 @@ collectionless_lists = [
|
|||
other_attributes = [
|
||||
"schedule",
|
||||
"sync_mode",
|
||||
"template",
|
||||
"test",
|
||||
"tmdb_person"
|
||||
]
|
||||
|
|
Loading…
Reference in a new issue