push notifiarr update

This commit is contained in:
meisnate12 2021-10-04 13:51:32 -04:00
parent cef150bec0
commit 8516ac10db
9 changed files with 638 additions and 401 deletions

View file

@ -76,7 +76,10 @@ summary_details = [
]
poster_details = ["url_poster", "tmdb_poster", "tmdb_profile", "tvdb_poster", "file_poster"]
background_details = ["url_background", "tmdb_background", "tvdb_background", "file_background"]
boolean_details = ["visible_library", "visible_home", "visible_shared", "show_filtered", "show_missing", "save_missing", "missing_only_released", "delete_below_minimum"]
boolean_details = [
"visible_library", "visible_home", "visible_shared", "show_filtered", "show_missing", "save_missing", "missing_only_released",
"delete_below_minimum", "notifiarr_collection_creation", "notifiarr_collection_addition", "notifiarr_collection_removing"
]
string_details = ["sort_title", "content_rating", "name_mapping"]
ignored_details = [
"smart_filter", "smart_label", "smart_url", "run_again", "schedule", "sync_mode", "template", "test",
@ -168,7 +171,10 @@ class CollectionBuilder:
"save_missing": self.library.save_missing,
"missing_only_released": self.library.missing_only_released,
"create_asset_folders": self.library.create_asset_folders,
"delete_below_minimum": self.library.delete_below_minimum
"delete_below_minimum": self.library.delete_below_minimum,
"notifiarr_collection_creation": self.library.notifiarr_collection_creation,
"notifiarr_collection_addition": self.library.notifiarr_collection_addition,
"notifiarr_collection_removing": self.library.notifiarr_collection_removing,
}
self.item_details = {}
self.radarr_details = {}
@ -183,6 +189,8 @@ class CollectionBuilder:
self.filtered_keys = {}
self.run_again_movies = []
self.run_again_shows = []
self.notifiarr_additions = []
self.notifiarr_removals = []
self.items = []
self.posters = {}
self.backgrounds = {}
@ -191,6 +199,8 @@ class CollectionBuilder:
self.minimum = self.library.collection_minimum
self.current_time = datetime.now()
self.current_year = self.current_time.year
self.exists = False
self.created = False
methods = {m.lower(): m for m in self.data}
@ -537,6 +547,7 @@ class CollectionBuilder:
elif not self.library.Sonarr and "sonarr" in method_name: raise Failed(f"Collection Error: {method_final} requires Sonarr to be configured")
elif not self.library.Tautulli and "tautulli" in method_name: raise Failed(f"Collection Error: {method_final} requires Tautulli to be configured")
elif not self.config.MyAnimeList and "mal" in method_name: raise Failed(f"Collection Error: {method_final} requires MyAnimeList to be configured")
elif not self.library.Notifiarr and "notifiarr" in method_name: raise Failed(f"Collection Error: {method_final} requires Notifiarr to be configured")
elif self.library.is_movie and method_name in show_only_builders: raise Failed(f"Collection Error: {method_final} attribute only works for show libraries")
elif self.library.is_show and method_name in movie_only_builders: raise Failed(f"Collection Error: {method_final} attribute only works for movie libraries")
elif self.library.is_show and method_name in plex.movie_only_searches: raise Failed(f"Collection Error: {method_final} plex search only works for movie libraries")
@ -617,6 +628,8 @@ class CollectionBuilder:
if self.sync and self.obj:
for item in self.library.get_collection_items(self.obj, self.smart_label_collection):
self.plex_map[item.ratingKey] = item
if self.obj:
self.exists = True
else:
self.obj = None
self.sync = False
@ -1122,7 +1135,7 @@ class CollectionBuilder:
rating_keys.append(input_id)
elif id_type == "tmdb" and not self.parts_collection:
if input_id in self.library.movie_map:
rating_keys.append(self.library.movie_map[input_id][0])
rating_keys.extend(self.library.movie_map[input_id])
elif input_id not in self.missing_movies:
self.missing_movies.append(input_id)
elif id_type in ["tvdb", "tmdb_show"] and not self.parts_collection:
@ -1133,12 +1146,12 @@ class CollectionBuilder:
logger.error(e)
continue
if input_id in self.library.show_map:
rating_keys.append(self.library.show_map[input_id][0])
rating_keys.extend(self.library.show_map[input_id])
elif input_id not in self.missing_shows:
self.missing_shows.append(input_id)
elif id_type == "imdb" and not self.parts_collection:
if input_id in self.library.imdb_map:
rating_keys.append(self.library.imdb_map[input_id][0])
rating_keys.extend(self.library.imdb_map[input_id])
else:
if self.do_missing:
try:
@ -1486,6 +1499,14 @@ class CollectionBuilder:
self.plex_map[current.ratingKey] = None
else:
self.library.alter_collection(current, name, smart_label_collection=self.smart_label_collection)
if self.details["notifiarr_collection_addition"]:
if self.library.is_movie and current.ratingKey in self.library.movie_rating_key_map:
add_id = self.library.movie_rating_key_map[current.ratingKey]
elif self.library.is_show and current.ratingKey in self.library.show_rating_key_map:
add_id = self.library.show_rating_key_map[current.ratingKey]
else:
add_id = None
self.notifiarr_additions.append({"title": current.title, "id": add_id})
util.print_end()
logger.info("")
logger.info(f"{total} {self.collection_level.capitalize()}{'s' if total > 1 else ''} Processed")
@ -1714,6 +1735,14 @@ class CollectionBuilder:
self.library.reload(item)
logger.info(f"{self.name} Collection | - | {self.item_title(item)}")
self.library.alter_collection(item, self.name, smart_label_collection=self.smart_label_collection, add=False)
if self.details["notifiarr_collection_removing"]:
if self.library.is_movie and item.ratingKey in self.library.movie_rating_key_map:
remove_id = self.library.movie_rating_key_map[item.ratingKey]
elif self.library.is_show and item.ratingKey in self.library.show_rating_key_map:
remove_id = self.library.show_rating_key_map[item.ratingKey]
else:
remove_id = None
self.notifiarr_removals.append({"title": item.title, "id": remove_id})
count_removed += 1
if count_removed > 0:
logger.info("")
@ -1835,6 +1864,8 @@ class CollectionBuilder:
except Failed:
raise Failed(f"Collection Error: Label: {self.name} was not added to any items in the Library")
self.obj = self.library.get_collection(self.name)
if not self.exists:
self.created = True
def update_details(self):
logger.info("")
@ -2002,10 +2033,26 @@ class CollectionBuilder:
self.library.move_item(self.obj, key, after=previous)
previous = key
def send_notifications(self):
if self.obj and (
(self.details["notifiarr_collection_creation"] and self.created) or
(self.details["notifiarr_collection_addition"] and len(self.notifiarr_additions) > 0) or
(self.details["notifiarr_collection_removing"] and len(self.notifiarr_removals) > 0)
):
self.obj.reload()
self.library.Notifiarr.plex_collection(
self.obj,
created=self.created,
additions=self.notifiarr_additions,
removals=self.notifiarr_removals
)
def run_collections_again(self):
self.obj = self.library.get_collection(self.name)
name, collection_items = self.library.get_collection_name_and_items(self.obj, self.smart_label_collection)
self.created = False
rating_keys = []
self.notifiarr_additions = []
for mm in self.run_again_movies:
if mm in self.library.movie_map:
rating_keys.extend(self.library.movie_map[mm])
@ -2025,6 +2072,14 @@ class CollectionBuilder:
else:
self.library.alter_collection(current, name, smart_label_collection=self.smart_label_collection)
logger.info(f"{name} Collection | + | {self.item_title(current)}")
if self.library.is_movie and current.ratingKey in self.library.movie_rating_key_map:
add_id = self.library.movie_rating_key_map[current.ratingKey]
elif self.library.is_show and current.ratingKey in self.library.show_rating_key_map:
add_id = self.library.show_rating_key_map[current.ratingKey]
else:
add_id = None
self.notifiarr_additions.append({"title": current.title, "id": add_id})
self.send_notifications()
logger.info(f"{len(rating_keys)} {self.collection_level.capitalize()}{'s' if len(rating_keys) > 1 else ''} Processed")
if len(self.run_again_movies) > 0:

View file

@ -1,4 +1,4 @@
import logging, os, requests
import base64, logging, os, requests
from datetime import datetime
from lxml import html
from modules import util, radarr, sonarr
@ -10,6 +10,7 @@ from modules.icheckmovies import ICheckMovies
from modules.imdb import IMDb
from modules.letterboxd import Letterboxd
from modules.mal import MyAnimeList
from modules.notifiarr import NotifiarrFactory
from modules.omdb import OMDb
from modules.plex import Plex
from modules.radarr import Radarr
@ -29,21 +30,22 @@ sync_modes = {"append": "Only Add Items to the Collection", "sync": "Add & Remov
mass_update_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb"}
class Config:
def __init__(self, default_dir, config_path=None, is_test=False, time_scheduled=None, requested_collections=None, requested_libraries=None, resume_from=None):
def __init__(self, default_dir, attrs):
logger.info("Locating config...")
if config_path and os.path.exists(config_path): self.config_path = os.path.abspath(config_path)
elif config_path and not os.path.exists(config_path): raise Failed(f"Config Error: config not found at {os.path.abspath(config_path)}")
config_file = attrs["config_file"]
if config_file and os.path.exists(config_file): self.config_path = os.path.abspath(config_file)
elif config_file and not os.path.exists(config_file): raise Failed(f"Config Error: config not found at {os.path.abspath(config_file)}")
elif os.path.exists(os.path.join(default_dir, "config.yml")): self.config_path = os.path.abspath(os.path.join(default_dir, "config.yml"))
else: raise Failed(f"Config Error: config not found at {os.path.abspath(default_dir)}")
logger.info(f"Using {self.config_path} as config")
self.default_dir = default_dir
self.test_mode = is_test
self.run_start_time = time_scheduled
self.run_hour = datetime.strptime(time_scheduled, "%H:%M").hour
self.requested_collections = util.get_list(requested_collections)
self.requested_libraries = util.get_list(requested_libraries)
self.resume_from = resume_from
self.test_mode = attrs["test"]
self.run_start_time = attrs["time"]
self.run_hour = datetime.strptime(attrs["time"], "%H:%M").hour
self.requested_collections = util.get_list(attrs["collections"])
self.requested_libraries = util.get_list(attrs["libraries"])
self.resume_from = attrs["resume"]
yaml.YAML().allow_duplicate_keys = True
try:
@ -87,6 +89,7 @@ class Config:
if "radarr" in new_config: new_config["radarr"] = new_config.pop("radarr")
if "sonarr" in new_config: new_config["sonarr"] = new_config.pop("sonarr")
if "omdb" in new_config: new_config["omdb"] = new_config.pop("omdb")
if "notifiarr" in new_config: new_config["notifiarr"] = new_config.pop("notifiarr")
if "trakt" in new_config: new_config["trakt"] = new_config.pop("trakt")
if "mal" in new_config: new_config["mal"] = new_config.pop("mal")
if "anidb" in new_config: new_config["anidb"] = new_config.pop("anidb")
@ -186,7 +189,10 @@ class Config:
"missing_only_released": check_for_attribute(self.data, "missing_only_released", parent="settings", var_type="bool", default=False),
"create_asset_folders": check_for_attribute(self.data, "create_asset_folders", parent="settings", var_type="bool", default=False),
"collection_minimum": check_for_attribute(self.data, "collection_minimum", parent="settings", var_type="int", default=1),
"delete_below_minimum": check_for_attribute(self.data, "delete_below_minimum", parent="settings", var_type="bool", default=False)
"delete_below_minimum": check_for_attribute(self.data, "delete_below_minimum", parent="settings", var_type="bool", default=False),
"notifiarr_collection_creation": check_for_attribute(self.data, "notifiarr_collection_creation", parent="settings", var_type="bool", default=False),
"notifiarr_collection_addition": check_for_attribute(self.data, "notifiarr_collection_addition", parent="settings", var_type="bool", default=False),
"notifiarr_collection_removing": check_for_attribute(self.data, "notifiarr_collection_removing", parent="settings", var_type="bool", default=False)
}
if self.general["cache"]:
util.separator()
@ -196,323 +202,383 @@ class Config:
util.separator()
self.TMDb = None
if "tmdb" in self.data:
logger.info("Connecting to TMDb...")
self.TMDb = TMDb(self, {
"apikey": check_for_attribute(self.data, "apikey", parent="tmdb", throw=True),
"language": check_for_attribute(self.data, "language", parent="tmdb", default="en")
})
logger.info(f"TMDb Connection {'Failed' if self.TMDb is None else 'Successful'}")
else:
raise Failed("Config Error: tmdb attribute not found")
util.separator()
self.OMDb = None
if "omdb" in self.data:
logger.info("Connecting to OMDb...")
self.NotifiarrFactory = None
if "notifiarr" in self.data:
logger.info("Connecting to Notifiarr...")
try:
self.OMDb = OMDb(self, {"apikey": check_for_attribute(self.data, "apikey", parent="omdb", throw=True)})
except Failed as e:
logger.error(e)
logger.info(f"OMDb Connection {'Failed' if self.OMDb is None else 'Successful'}")
else:
logger.warning("omdb attribute not found")
util.separator()
self.Trakt = None
if "trakt" in self.data:
logger.info("Connecting to Trakt...")
try:
self.Trakt = Trakt(self, {
"client_id": check_for_attribute(self.data, "client_id", parent="trakt", throw=True),
"client_secret": check_for_attribute(self.data, "client_secret", parent="trakt", throw=True),
"config_path": self.config_path,
"authorization": self.data["trakt"]["authorization"] if "authorization" in self.data["trakt"] else None
self.NotifiarrFactory = NotifiarrFactory(self, {
"apikey": check_for_attribute(self.data, "apikey", parent="notifiarr", throw=True),
"error_notification": check_for_attribute(self.data, "error_notification", parent="notifiarr", var_type="bool", default=True),
"develop": check_for_attribute(self.data, "develop", parent="notifiarr", var_type="bool", default=False, do_print=False, save=False),
"test": check_for_attribute(self.data, "test", parent="notifiarr", var_type="bool", default=False, do_print=False, save=False)
})
except Failed as e:
logger.error(e)
logger.info(f"Trakt Connection {'Failed' if self.Trakt is None else 'Successful'}")
logger.info(f"Notifiarr Connection {'Failed' if self.NotifiarrFactory is None else 'Successful'}")
else:
logger.warning("trakt attribute not found")
logger.warning("notifiarr attribute not found")
self.errors = []
util.separator()
self.MyAnimeList = None
if "mal" in self.data:
logger.info("Connecting to My Anime List...")
try:
self.MyAnimeList = MyAnimeList(self, {
"client_id": check_for_attribute(self.data, "client_id", parent="mal", throw=True),
"client_secret": check_for_attribute(self.data, "client_secret", parent="mal", throw=True),
"config_path": self.config_path,
"authorization": self.data["mal"]["authorization"] if "authorization" in self.data["mal"] else None
try:
self.TMDb = None
if "tmdb" in self.data:
logger.info("Connecting to TMDb...")
self.TMDb = TMDb(self, {
"apikey": check_for_attribute(self.data, "apikey", parent="tmdb", throw=True),
"language": check_for_attribute(self.data, "language", parent="tmdb", default="en")
})
except Failed as e:
logger.error(e)
logger.info(f"My Anime List Connection {'Failed' if self.MyAnimeList is None else 'Successful'}")
else:
logger.warning("mal attribute not found")
logger.info(f"TMDb Connection {'Failed' if self.TMDb is None else 'Successful'}")
else:
raise Failed("Config Error: tmdb attribute not found")
util.separator()
self.AniDB = None
if "anidb" in self.data:
util.separator()
logger.info("Connecting to AniDB...")
try:
self.AniDB = AniDB(self, {
"username": check_for_attribute(self.data, "username", parent="anidb", throw=True),
"password": check_for_attribute(self.data, "password", parent="anidb", throw=True)
self.OMDb = None
if "omdb" in self.data:
logger.info("Connecting to OMDb...")
try:
self.OMDb = OMDb(self, {"apikey": check_for_attribute(self.data, "apikey", parent="omdb", throw=True)})
except Failed as e:
self.errors.append(e)
logger.error(e)
logger.info(f"OMDb Connection {'Failed' if self.OMDb is None else 'Successful'}")
else:
logger.warning("omdb attribute not found")
util.separator()
self.Trakt = None
if "trakt" in self.data:
logger.info("Connecting to Trakt...")
try:
self.Trakt = Trakt(self, {
"client_id": check_for_attribute(self.data, "client_id", parent="trakt", throw=True),
"client_secret": check_for_attribute(self.data, "client_secret", parent="trakt", throw=True),
"config_path": self.config_path,
"authorization": self.data["trakt"]["authorization"] if "authorization" in self.data["trakt"] else None
})
except Failed as e:
logger.error(e)
logger.info(f"My Anime List Connection {'Failed Continuing as Guest ' if self.MyAnimeList is None else 'Successful'}")
if self.AniDB is None:
self.AniDB = AniDB(self, None)
except Failed as e:
self.errors.append(e)
logger.error(e)
logger.info(f"Trakt Connection {'Failed' if self.Trakt is None else 'Successful'}")
else:
logger.warning("trakt attribute not found")
self.TVDb = TVDb(self)
self.IMDb = IMDb(self)
self.Convert = Convert(self)
self.AniList = AniList(self)
self.Letterboxd = Letterboxd(self)
self.ICheckMovies = ICheckMovies(self)
self.StevenLu = StevenLu(self)
util.separator()
logger.info("Connecting to Plex Libraries...")
self.general["plex"] = {
"url": check_for_attribute(self.data, "url", parent="plex", var_type="url", default_is_none=True),
"token": check_for_attribute(self.data, "token", parent="plex", default_is_none=True),
"timeout": check_for_attribute(self.data, "timeout", parent="plex", var_type="int", default=60),
"clean_bundles": check_for_attribute(self.data, "clean_bundles", parent="plex", var_type="bool", default=False),
"empty_trash": check_for_attribute(self.data, "empty_trash", parent="plex", var_type="bool", default=False),
"optimize": check_for_attribute(self.data, "optimize", parent="plex", var_type="bool", default=False)
}
self.general["radarr"] = {
"url": check_for_attribute(self.data, "url", parent="radarr", var_type="url", default_is_none=True),
"token": check_for_attribute(self.data, "token", parent="radarr", default_is_none=True),
"add": check_for_attribute(self.data, "add", parent="radarr", var_type="bool", default=False),
"add_existing": check_for_attribute(self.data, "add_existing", parent="radarr", var_type="bool", default=False),
"root_folder_path": check_for_attribute(self.data, "root_folder_path", parent="radarr", default_is_none=True),
"monitor": check_for_attribute(self.data, "monitor", parent="radarr", var_type="bool", default=True),
"availability": check_for_attribute(self.data, "availability", parent="radarr", test_list=radarr.availability_descriptions, default="announced"),
"quality_profile": check_for_attribute(self.data, "quality_profile", parent="radarr", default_is_none=True),
"tag": check_for_attribute(self.data, "tag", parent="radarr", var_type="lower_list", default_is_none=True),
"search": check_for_attribute(self.data, "search", parent="radarr", var_type="bool", default=False)
}
self.general["sonarr"] = {
"url": check_for_attribute(self.data, "url", parent="sonarr", var_type="url", default_is_none=True),
"token": check_for_attribute(self.data, "token", parent="sonarr", default_is_none=True),
"add": check_for_attribute(self.data, "add", parent="sonarr", var_type="bool", default=False),
"add_existing": check_for_attribute(self.data, "add_existing", parent="sonarr", var_type="bool", default=False),
"root_folder_path": check_for_attribute(self.data, "root_folder_path", parent="sonarr", default_is_none=True),
"monitor": check_for_attribute(self.data, "monitor", parent="sonarr", test_list=sonarr.monitor_descriptions, default="all"),
"quality_profile": check_for_attribute(self.data, "quality_profile", parent="sonarr", default_is_none=True),
"language_profile": check_for_attribute(self.data, "language_profile", parent="sonarr", default_is_none=True),
"series_type": check_for_attribute(self.data, "series_type", parent="sonarr", test_list=sonarr.series_type_descriptions, default="standard"),
"season_folder": check_for_attribute(self.data, "season_folder", parent="sonarr", var_type="bool", default=True),
"tag": check_for_attribute(self.data, "tag", parent="sonarr", var_type="lower_list", default_is_none=True),
"search": check_for_attribute(self.data, "search", parent="sonarr", var_type="bool", default=False),
"cutoff_search": check_for_attribute(self.data, "cutoff_search", parent="sonarr", var_type="bool", default=False)
}
self.general["tautulli"] = {
"url": check_for_attribute(self.data, "url", parent="tautulli", var_type="url", default_is_none=True),
"apikey": check_for_attribute(self.data, "apikey", parent="tautulli", default_is_none=True)
}
self.libraries = []
libs = check_for_attribute(self.data, "libraries", throw=True)
for library_name, lib in libs.items():
if self.requested_libraries and library_name not in self.requested_libraries:
continue
util.separator()
params = {
"mapping_name": str(library_name),
"name": str(lib["library_name"]) if lib and "library_name" in lib and lib["library_name"] else str(library_name)
self.MyAnimeList = None
if "mal" in self.data:
logger.info("Connecting to My Anime List...")
try:
self.MyAnimeList = MyAnimeList(self, {
"client_id": check_for_attribute(self.data, "client_id", parent="mal", throw=True),
"client_secret": check_for_attribute(self.data, "client_secret", parent="mal", throw=True),
"config_path": self.config_path,
"authorization": self.data["mal"]["authorization"] if "authorization" in self.data["mal"] else None
})
except Failed as e:
self.errors.append(e)
logger.error(e)
logger.info(f"My Anime List Connection {'Failed' if self.MyAnimeList is None else 'Successful'}")
else:
logger.warning("mal attribute not found")
util.separator()
self.AniDB = None
if "anidb" in self.data:
util.separator()
logger.info("Connecting to AniDB...")
try:
self.AniDB = AniDB(self, {
"username": check_for_attribute(self.data, "username", parent="anidb", throw=True),
"password": check_for_attribute(self.data, "password", parent="anidb", throw=True)
})
except Failed as e:
self.errors.append(e)
logger.error(e)
logger.info(f"My Anime List Connection {'Failed Continuing as Guest ' if self.MyAnimeList is None else 'Successful'}")
if self.AniDB is None:
self.AniDB = AniDB(self, None)
self.TVDb = TVDb(self)
self.IMDb = IMDb(self)
self.Convert = Convert(self)
self.AniList = AniList(self)
self.Letterboxd = Letterboxd(self)
self.ICheckMovies = ICheckMovies(self)
self.StevenLu = StevenLu(self)
util.separator()
logger.info("Connecting to Plex Libraries...")
self.general["plex"] = {
"url": check_for_attribute(self.data, "url", parent="plex", var_type="url", default_is_none=True),
"token": check_for_attribute(self.data, "token", parent="plex", default_is_none=True),
"timeout": check_for_attribute(self.data, "timeout", parent="plex", var_type="int", default=60),
"clean_bundles": check_for_attribute(self.data, "clean_bundles", parent="plex", var_type="bool", default=False),
"empty_trash": check_for_attribute(self.data, "empty_trash", parent="plex", var_type="bool", default=False),
"optimize": check_for_attribute(self.data, "optimize", parent="plex", var_type="bool", default=False)
}
self.general["radarr"] = {
"url": check_for_attribute(self.data, "url", parent="radarr", var_type="url", default_is_none=True),
"token": check_for_attribute(self.data, "token", parent="radarr", default_is_none=True),
"add": check_for_attribute(self.data, "add", parent="radarr", var_type="bool", default=False),
"add_existing": check_for_attribute(self.data, "add_existing", parent="radarr", var_type="bool", default=False),
"root_folder_path": check_for_attribute(self.data, "root_folder_path", parent="radarr", default_is_none=True),
"monitor": check_for_attribute(self.data, "monitor", parent="radarr", var_type="bool", default=True),
"availability": check_for_attribute(self.data, "availability", parent="radarr", test_list=radarr.availability_descriptions, default="announced"),
"quality_profile": check_for_attribute(self.data, "quality_profile", parent="radarr", default_is_none=True),
"tag": check_for_attribute(self.data, "tag", parent="radarr", var_type="lower_list", default_is_none=True),
"search": check_for_attribute(self.data, "search", parent="radarr", var_type="bool", default=False)
}
self.general["sonarr"] = {
"url": check_for_attribute(self.data, "url", parent="sonarr", var_type="url", default_is_none=True),
"token": check_for_attribute(self.data, "token", parent="sonarr", default_is_none=True),
"add": check_for_attribute(self.data, "add", parent="sonarr", var_type="bool", default=False),
"add_existing": check_for_attribute(self.data, "add_existing", parent="sonarr", var_type="bool", default=False),
"root_folder_path": check_for_attribute(self.data, "root_folder_path", parent="sonarr", default_is_none=True),
"monitor": check_for_attribute(self.data, "monitor", parent="sonarr", test_list=sonarr.monitor_descriptions, default="all"),
"quality_profile": check_for_attribute(self.data, "quality_profile", parent="sonarr", default_is_none=True),
"language_profile": check_for_attribute(self.data, "language_profile", parent="sonarr", default_is_none=True),
"series_type": check_for_attribute(self.data, "series_type", parent="sonarr", test_list=sonarr.series_type_descriptions, default="standard"),
"season_folder": check_for_attribute(self.data, "season_folder", parent="sonarr", var_type="bool", default=True),
"tag": check_for_attribute(self.data, "tag", parent="sonarr", var_type="lower_list", default_is_none=True),
"search": check_for_attribute(self.data, "search", parent="sonarr", var_type="bool", default=False),
"cutoff_search": check_for_attribute(self.data, "cutoff_search", parent="sonarr", var_type="bool", default=False)
}
self.general["tautulli"] = {
"url": check_for_attribute(self.data, "url", parent="tautulli", var_type="url", default_is_none=True),
"apikey": check_for_attribute(self.data, "apikey", parent="tautulli", default_is_none=True)
}
display_name = f"{params['name']} ({params['mapping_name']})" if lib and "library_name" in lib and lib["library_name"] else params["mapping_name"]
util.separator(f"{display_name} Configuration")
logger.info("")
logger.info(f"Connecting to {display_name} Library...")
self.libraries = []
libs = check_for_attribute(self.data, "libraries", throw=True)
params["asset_directory"] = check_for_attribute(lib, "asset_directory", parent="settings", var_type="list_path", default=self.general["asset_directory"], default_is_none=True, save=False)
if params["asset_directory"] is None:
logger.warning("Config Warning: Assets will not be used asset_directory attribute must be set under config or under this specific Library")
params["asset_folders"] = check_for_attribute(lib, "asset_folders", parent="settings", var_type="bool", default=self.general["asset_folders"], do_print=False, save=False)
params["assets_for_all"] = check_for_attribute(lib, "assets_for_all", parent="settings", var_type="bool", default=self.general["assets_for_all"], do_print=False, save=False)
params["sync_mode"] = check_for_attribute(lib, "sync_mode", parent="settings", test_list=sync_modes, default=self.general["sync_mode"], do_print=False, save=False)
params["show_unmanaged"] = check_for_attribute(lib, "show_unmanaged", parent="settings", var_type="bool", default=self.general["show_unmanaged"], do_print=False, save=False)
params["show_filtered"] = check_for_attribute(lib, "show_filtered", parent="settings", var_type="bool", default=self.general["show_filtered"], do_print=False, save=False)
params["show_missing"] = check_for_attribute(lib, "show_missing", parent="settings", var_type="bool", default=self.general["show_missing"], do_print=False, save=False)
params["save_missing"] = check_for_attribute(lib, "save_missing", parent="settings", var_type="bool", default=self.general["save_missing"], do_print=False, save=False)
params["missing_only_released"] = check_for_attribute(lib, "missing_only_released", parent="settings", var_type="bool", default=self.general["missing_only_released"], do_print=False, save=False)
params["create_asset_folders"] = check_for_attribute(lib, "create_asset_folders", parent="settings", var_type="bool", default=self.general["create_asset_folders"], do_print=False, save=False)
params["collection_minimum"] = check_for_attribute(lib, "collection_minimum", parent="settings", var_type="int", default=self.general["collection_minimum"], do_print=False, save=False)
params["delete_below_minimum"] = check_for_attribute(lib, "delete_below_minimum", parent="settings", var_type="bool", default=self.general["delete_below_minimum"], do_print=False, save=False)
params["mass_genre_update"] = check_for_attribute(lib, "mass_genre_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=lib and "mass_genre_update" in lib)
if self.OMDb is None and params["mass_genre_update"] == "omdb":
params["mass_genre_update"] = None
logger.error("Config Error: mass_genre_update cannot be omdb without a successful OMDb Connection")
params["mass_audience_rating_update"] = check_for_attribute(lib, "mass_audience_rating_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=lib and "mass_audience_rating_update" in lib)
if self.OMDb is None and params["mass_audience_rating_update"] == "omdb":
params["mass_audience_rating_update"] = None
logger.error("Config Error: mass_audience_rating_update cannot be omdb without a successful OMDb Connection")
params["mass_critic_rating_update"] = check_for_attribute(lib, "mass_critic_rating_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=lib and "mass_audience_rating_update" in lib)
if self.OMDb is None and params["mass_critic_rating_update"] == "omdb":
params["mass_critic_rating_update"] = None
logger.error("Config Error: mass_critic_rating_update cannot be omdb without a successful OMDb Connection")
params["mass_trakt_rating_update"] = check_for_attribute(lib, "mass_trakt_rating_update", var_type="bool", default=False, save=False, do_print=lib and "mass_trakt_rating_update" in lib)
if self.Trakt is None and params["mass_trakt_rating_update"]:
params["mass_trakt_rating_update"] = None
logger.error("Config Error: mass_trakt_rating_update cannot run without a successful Trakt Connection")
params["split_duplicates"] = check_for_attribute(lib, "split_duplicates", var_type="bool", default=False, save=False, do_print=lib and "split_duplicates" in lib)
params["radarr_add_all"] = check_for_attribute(lib, "radarr_add_all", var_type="bool", default=False, save=False, do_print=lib and "radarr_add_all" in lib)
params["sonarr_add_all"] = check_for_attribute(lib, "sonarr_add_all", var_type="bool", default=False, save=False, do_print=lib and "sonarr_add_all" in lib)
try:
if lib and "metadata_path" in lib:
params["metadata_path"] = []
if lib["metadata_path"] is None:
raise Failed("Config Error: metadata_path attribute is blank")
paths_to_check = lib["metadata_path"] if isinstance(lib["metadata_path"], list) else [lib["metadata_path"]]
for path in paths_to_check:
if isinstance(path, dict):
def check_dict(attr, name):
if attr in path:
if path[attr] is None:
logger.error(f"Config Error: metadata_path {attr} is blank")
else:
params["metadata_path"].append((name, path[attr]))
check_dict("url", "URL")
check_dict("git", "Git")
check_dict("file", "File")
check_dict("folder", "Folder")
else:
params["metadata_path"].append(("File", path))
else:
params["metadata_path"] = [("File", os.path.join(default_dir, f"{library_name}.yml"))]
params["default_dir"] = default_dir
params["plex"] = {
"url": check_for_attribute(lib, "url", parent="plex", var_type="url", default=self.general["plex"]["url"], req_default=True, save=False),
"token": check_for_attribute(lib, "token", parent="plex", default=self.general["plex"]["token"], req_default=True, save=False),
"timeout": check_for_attribute(lib, "timeout", parent="plex", var_type="int", default=self.general["plex"]["timeout"], save=False),
"clean_bundles": check_for_attribute(lib, "clean_bundles", parent="plex", var_type="bool", default=self.general["plex"]["clean_bundles"], save=False),
"empty_trash": check_for_attribute(lib, "empty_trash", parent="plex", var_type="bool", default=self.general["plex"]["empty_trash"], save=False),
"optimize": check_for_attribute(lib, "optimize", parent="plex", var_type="bool", default=self.general["plex"]["optimize"], save=False)
for library_name, lib in libs.items():
if self.requested_libraries and library_name not in self.requested_libraries:
continue
util.separator()
params = {
"mapping_name": str(library_name),
"name": str(lib["library_name"]) if lib and "library_name" in lib and lib["library_name"] else str(library_name)
}
library = Plex(self, params)
logger.info("")
logger.info(f"{display_name} Library Connection Successful")
except Failed as e:
util.print_stacktrace()
util.print_multiline(e, error=True)
logger.info(f"{display_name} Library Connection Failed")
continue
display_name = f"{params['name']} ({params['mapping_name']})" if lib and "library_name" in lib and lib["library_name"] else params["mapping_name"]
if self.general["radarr"]["url"] or (lib and "radarr" in lib):
logger.info("")
util.separator("Radarr Configuration", space=False, border=False)
logger.info("")
logger.info(f"Connecting to {display_name} library's Radarr...")
util.separator(f"{display_name} Configuration")
logger.info("")
logger.info(f"Connecting to {display_name} Library...")
params["asset_directory"] = check_for_attribute(lib, "asset_directory", parent="settings", var_type="list_path", default=self.general["asset_directory"], default_is_none=True, save=False)
if params["asset_directory"] is None:
logger.warning("Config Warning: Assets will not be used asset_directory attribute must be set under config or under this specific Library")
params["asset_folders"] = check_for_attribute(lib, "asset_folders", parent="settings", var_type="bool", default=self.general["asset_folders"], do_print=False, save=False)
params["assets_for_all"] = check_for_attribute(lib, "assets_for_all", parent="settings", var_type="bool", default=self.general["assets_for_all"], do_print=False, save=False)
params["sync_mode"] = check_for_attribute(lib, "sync_mode", parent="settings", test_list=sync_modes, default=self.general["sync_mode"], do_print=False, save=False)
params["show_unmanaged"] = check_for_attribute(lib, "show_unmanaged", parent="settings", var_type="bool", default=self.general["show_unmanaged"], do_print=False, save=False)
params["show_filtered"] = check_for_attribute(lib, "show_filtered", parent="settings", var_type="bool", default=self.general["show_filtered"], do_print=False, save=False)
params["show_missing"] = check_for_attribute(lib, "show_missing", parent="settings", var_type="bool", default=self.general["show_missing"], do_print=False, save=False)
params["save_missing"] = check_for_attribute(lib, "save_missing", parent="settings", var_type="bool", default=self.general["save_missing"], do_print=False, save=False)
params["missing_only_released"] = check_for_attribute(lib, "missing_only_released", parent="settings", var_type="bool", default=self.general["missing_only_released"], do_print=False, save=False)
params["create_asset_folders"] = check_for_attribute(lib, "create_asset_folders", parent="settings", var_type="bool", default=self.general["create_asset_folders"], do_print=False, save=False)
params["collection_minimum"] = check_for_attribute(lib, "collection_minimum", parent="settings", var_type="int", default=self.general["collection_minimum"], do_print=False, save=False)
params["delete_below_minimum"] = check_for_attribute(lib, "delete_below_minimum", parent="settings", var_type="bool", default=self.general["delete_below_minimum"], do_print=False, save=False)
params["notifiarr_collection_creation"] = check_for_attribute(lib, "notifiarr_collection_creation", parent="settings", var_type="bool", default=self.general["notifiarr_collection_creation"], do_print=False, save=False)
params["notifiarr_collection_addition"] = check_for_attribute(lib, "notifiarr_collection_addition", parent="settings", var_type="bool", default=self.general["notifiarr_collection_addition"], do_print=False, save=False)
params["notifiarr_collection_removing"] = check_for_attribute(lib, "notifiarr_collection_removing", parent="settings", var_type="bool", default=self.general["notifiarr_collection_removing"], do_print=False, save=False)
params["mass_genre_update"] = check_for_attribute(lib, "mass_genre_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=lib and "mass_genre_update" in lib)
if self.OMDb is None and params["mass_genre_update"] == "omdb":
params["mass_genre_update"] = None
e = "Config Error: mass_genre_update cannot be omdb without a successful OMDb Connection"
self.errors.append(e)
logger.error(e)
params["mass_audience_rating_update"] = check_for_attribute(lib, "mass_audience_rating_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=lib and "mass_audience_rating_update" in lib)
if self.OMDb is None and params["mass_audience_rating_update"] == "omdb":
params["mass_audience_rating_update"] = None
e = "Config Error: mass_audience_rating_update cannot be omdb without a successful OMDb Connection"
self.errors.append(e)
logger.error(e)
params["mass_critic_rating_update"] = check_for_attribute(lib, "mass_critic_rating_update", test_list=mass_update_options, default_is_none=True, save=False, do_print=lib and "mass_audience_rating_update" in lib)
if self.OMDb is None and params["mass_critic_rating_update"] == "omdb":
params["mass_critic_rating_update"] = None
e = "Config Error: mass_critic_rating_update cannot be omdb without a successful OMDb Connection"
self.errors.append(e)
logger.error(e)
params["mass_trakt_rating_update"] = check_for_attribute(lib, "mass_trakt_rating_update", var_type="bool", default=False, save=False, do_print=lib and "mass_trakt_rating_update" in lib)
if self.Trakt is None and params["mass_trakt_rating_update"]:
params["mass_trakt_rating_update"] = None
e = "Config Error: mass_trakt_rating_update cannot run without a successful Trakt Connection"
self.errors.append(e)
logger.error(e)
params["split_duplicates"] = check_for_attribute(lib, "split_duplicates", var_type="bool", default=False, save=False, do_print=lib and "split_duplicates" in lib)
params["radarr_add_all"] = check_for_attribute(lib, "radarr_add_all", var_type="bool", default=False, save=False, do_print=lib and "radarr_add_all" in lib)
params["sonarr_add_all"] = check_for_attribute(lib, "sonarr_add_all", var_type="bool", default=False, save=False, do_print=lib and "sonarr_add_all" in lib)
try:
library.Radarr = Radarr(self, {
"url": check_for_attribute(lib, "url", parent="radarr", var_type="url", default=self.general["radarr"]["url"], req_default=True, save=False),
"token": check_for_attribute(lib, "token", parent="radarr", default=self.general["radarr"]["token"], req_default=True, save=False),
"add": check_for_attribute(lib, "add", parent="radarr", var_type="bool", default=self.general["radarr"]["add"], save=False),
"add_existing": check_for_attribute(lib, "add_existing", parent="radarr", var_type="bool", default=self.general["radarr"]["add_existing"], save=False),
"root_folder_path": check_for_attribute(lib, "root_folder_path", parent="radarr", default=self.general["radarr"]["root_folder_path"], req_default=True, save=False),
"monitor": check_for_attribute(lib, "monitor", parent="radarr", var_type="bool", default=self.general["radarr"]["monitor"], save=False),
"availability": check_for_attribute(lib, "availability", parent="radarr", test_list=radarr.availability_descriptions, default=self.general["radarr"]["availability"], save=False),
"quality_profile": check_for_attribute(lib, "quality_profile", parent="radarr",default=self.general["radarr"]["quality_profile"], req_default=True, save=False),
"tag": check_for_attribute(lib, "tag", parent="radarr", var_type="lower_list", default=self.general["radarr"]["tag"], default_is_none=True, save=False),
"search": check_for_attribute(lib, "search", parent="radarr", var_type="bool", default=self.general["radarr"]["search"], save=False)
})
if lib and "metadata_path" in lib:
params["metadata_path"] = []
if lib["metadata_path"] is None:
raise Failed("Config Error: metadata_path attribute is blank")
paths_to_check = lib["metadata_path"] if isinstance(lib["metadata_path"], list) else [lib["metadata_path"]]
for path in paths_to_check:
if isinstance(path, dict):
def check_dict(attr, name):
if attr in path:
if path[attr] is None:
e = f"Config Error: metadata_path {attr} is blank"
self.errors.append(e)
logger.error(e)
else:
params["metadata_path"].append((name, path[attr]))
check_dict("url", "URL")
check_dict("git", "Git")
check_dict("file", "File")
check_dict("folder", "Folder")
else:
params["metadata_path"].append(("File", path))
else:
params["metadata_path"] = [("File", os.path.join(default_dir, f"{library_name}.yml"))]
params["default_dir"] = default_dir
params["plex"] = {
"url": check_for_attribute(lib, "url", parent="plex", var_type="url", default=self.general["plex"]["url"], req_default=True, save=False),
"token": check_for_attribute(lib, "token", parent="plex", default=self.general["plex"]["token"], req_default=True, save=False),
"timeout": check_for_attribute(lib, "timeout", parent="plex", var_type="int", default=self.general["plex"]["timeout"], save=False),
"clean_bundles": check_for_attribute(lib, "clean_bundles", parent="plex", var_type="bool", default=self.general["plex"]["clean_bundles"], save=False),
"empty_trash": check_for_attribute(lib, "empty_trash", parent="plex", var_type="bool", default=self.general["plex"]["empty_trash"], save=False),
"optimize": check_for_attribute(lib, "optimize", parent="plex", var_type="bool", default=self.general["plex"]["optimize"], save=False)
}
library = Plex(self, params)
logger.info("")
logger.info(f"{display_name} Library Connection Successful")
except Failed as e:
self.errors.append(e)
util.print_stacktrace()
util.print_multiline(e, error=True)
logger.info(f"{display_name} Library Connection Failed")
continue
if self.general["radarr"]["url"] or (lib and "radarr" in lib):
logger.info("")
logger.info(f"{display_name} library's Radarr Connection {'Failed' if library.Radarr is None else 'Successful'}")
if self.general["sonarr"]["url"] or (lib and "sonarr" in lib):
logger.info("")
util.separator("Sonarr Configuration", space=False, border=False)
logger.info("")
logger.info(f"Connecting to {display_name} library's Sonarr...")
logger.info("")
try:
library.Sonarr = Sonarr(self, {
"url": check_for_attribute(lib, "url", parent="sonarr", var_type="url", default=self.general["sonarr"]["url"], req_default=True, save=False),
"token": check_for_attribute(lib, "token", parent="sonarr", default=self.general["sonarr"]["token"], req_default=True, save=False),
"add": check_for_attribute(lib, "add", parent="sonarr", var_type="bool", default=self.general["sonarr"]["add"], save=False),
"add_existing": check_for_attribute(lib, "add_existing", parent="sonarr", var_type="bool", default=self.general["sonarr"]["add_existing"], save=False),
"root_folder_path": check_for_attribute(lib, "root_folder_path", parent="sonarr", default=self.general["sonarr"]["root_folder_path"], req_default=True, save=False),
"monitor": check_for_attribute(lib, "monitor", parent="sonarr", test_list=sonarr.monitor_descriptions, default=self.general["sonarr"]["monitor"], save=False),
"quality_profile": check_for_attribute(lib, "quality_profile", parent="sonarr", default=self.general["sonarr"]["quality_profile"], req_default=True, save=False),
"language_profile": check_for_attribute(lib, "language_profile", parent="sonarr", default=self.general["sonarr"]["language_profile"], save=False) if self.general["sonarr"]["language_profile"] else check_for_attribute(lib, "language_profile", parent="sonarr", default_is_none=True, save=False),
"series_type": check_for_attribute(lib, "series_type", parent="sonarr", test_list=sonarr.series_type_descriptions, default=self.general["sonarr"]["series_type"], save=False),
"season_folder": check_for_attribute(lib, "season_folder", parent="sonarr", var_type="bool", default=self.general["sonarr"]["season_folder"], save=False),
"tag": check_for_attribute(lib, "tag", parent="sonarr", var_type="lower_list", default=self.general["sonarr"]["tag"], default_is_none=True, save=False),
"search": check_for_attribute(lib, "search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["search"], save=False),
"cutoff_search": check_for_attribute(lib, "cutoff_search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["cutoff_search"], save=False)
})
except Failed as e:
util.print_stacktrace()
util.print_multiline(e, error=True)
util.separator("Radarr Configuration", space=False, border=False)
logger.info("")
logger.info(f"{display_name} library's Sonarr Connection {'Failed' if library.Sonarr is None else 'Successful'}")
if self.general["tautulli"]["url"] or (lib and "tautulli" in lib):
logger.info("")
util.separator("Tautulli Configuration", space=False, border=False)
logger.info("")
logger.info(f"Connecting to {display_name} library's Tautulli...")
logger.info("")
try:
library.Tautulli = Tautulli(self, {
"url": check_for_attribute(lib, "url", parent="tautulli", var_type="url", default=self.general["tautulli"]["url"], req_default=True, save=False),
"apikey": check_for_attribute(lib, "apikey", parent="tautulli", default=self.general["tautulli"]["apikey"], req_default=True, save=False)
})
except Failed as e:
util.print_stacktrace()
util.print_multiline(e, error=True)
logger.info(f"Connecting to {display_name} library's Radarr...")
logger.info("")
logger.info(f"{display_name} library's Tautulli Connection {'Failed' if library.Tautulli is None else 'Successful'}")
try:
library.Radarr = Radarr(self, {
"url": check_for_attribute(lib, "url", parent="radarr", var_type="url", default=self.general["radarr"]["url"], req_default=True, save=False),
"token": check_for_attribute(lib, "token", parent="radarr", default=self.general["radarr"]["token"], req_default=True, save=False),
"add": check_for_attribute(lib, "add", parent="radarr", var_type="bool", default=self.general["radarr"]["add"], save=False),
"add_existing": check_for_attribute(lib, "add_existing", parent="radarr", var_type="bool", default=self.general["radarr"]["add_existing"], save=False),
"root_folder_path": check_for_attribute(lib, "root_folder_path", parent="radarr", default=self.general["radarr"]["root_folder_path"], req_default=True, save=False),
"monitor": check_for_attribute(lib, "monitor", parent="radarr", var_type="bool", default=self.general["radarr"]["monitor"], save=False),
"availability": check_for_attribute(lib, "availability", parent="radarr", test_list=radarr.availability_descriptions, default=self.general["radarr"]["availability"], save=False),
"quality_profile": check_for_attribute(lib, "quality_profile", parent="radarr",default=self.general["radarr"]["quality_profile"], req_default=True, save=False),
"tag": check_for_attribute(lib, "tag", parent="radarr", var_type="lower_list", default=self.general["radarr"]["tag"], default_is_none=True, save=False),
"search": check_for_attribute(lib, "search", parent="radarr", var_type="bool", default=self.general["radarr"]["search"], save=False)
})
except Failed as e:
self.errors.append(e)
util.print_stacktrace()
util.print_multiline(e, error=True)
logger.info("")
logger.info(f"{display_name} library's Radarr Connection {'Failed' if library.Radarr is None else 'Successful'}")
logger.info("")
self.libraries.append(library)
if self.general["sonarr"]["url"] or (lib and "sonarr" in lib):
logger.info("")
util.separator("Sonarr Configuration", space=False, border=False)
logger.info("")
logger.info(f"Connecting to {display_name} library's Sonarr...")
logger.info("")
try:
library.Sonarr = Sonarr(self, {
"url": check_for_attribute(lib, "url", parent="sonarr", var_type="url", default=self.general["sonarr"]["url"], req_default=True, save=False),
"token": check_for_attribute(lib, "token", parent="sonarr", default=self.general["sonarr"]["token"], req_default=True, save=False),
"add": check_for_attribute(lib, "add", parent="sonarr", var_type="bool", default=self.general["sonarr"]["add"], save=False),
"add_existing": check_for_attribute(lib, "add_existing", parent="sonarr", var_type="bool", default=self.general["sonarr"]["add_existing"], save=False),
"root_folder_path": check_for_attribute(lib, "root_folder_path", parent="sonarr", default=self.general["sonarr"]["root_folder_path"], req_default=True, save=False),
"monitor": check_for_attribute(lib, "monitor", parent="sonarr", test_list=sonarr.monitor_descriptions, default=self.general["sonarr"]["monitor"], save=False),
"quality_profile": check_for_attribute(lib, "quality_profile", parent="sonarr", default=self.general["sonarr"]["quality_profile"], req_default=True, save=False),
"language_profile": check_for_attribute(lib, "language_profile", parent="sonarr", default=self.general["sonarr"]["language_profile"], save=False) if self.general["sonarr"]["language_profile"] else check_for_attribute(lib, "language_profile", parent="sonarr", default_is_none=True, save=False),
"series_type": check_for_attribute(lib, "series_type", parent="sonarr", test_list=sonarr.series_type_descriptions, default=self.general["sonarr"]["series_type"], save=False),
"season_folder": check_for_attribute(lib, "season_folder", parent="sonarr", var_type="bool", default=self.general["sonarr"]["season_folder"], save=False),
"tag": check_for_attribute(lib, "tag", parent="sonarr", var_type="lower_list", default=self.general["sonarr"]["tag"], default_is_none=True, save=False),
"search": check_for_attribute(lib, "search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["search"], save=False),
"cutoff_search": check_for_attribute(lib, "cutoff_search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["cutoff_search"], save=False)
})
except Failed as e:
self.errors.append(e)
util.print_stacktrace()
util.print_multiline(e, error=True)
logger.info("")
logger.info(f"{display_name} library's Sonarr Connection {'Failed' if library.Sonarr is None else 'Successful'}")
util.separator()
if self.general["tautulli"]["url"] or (lib and "tautulli" in lib):
logger.info("")
util.separator("Tautulli Configuration", space=False, border=False)
logger.info("")
logger.info(f"Connecting to {display_name} library's Tautulli...")
logger.info("")
try:
library.Tautulli = Tautulli(self, {
"url": check_for_attribute(lib, "url", parent="tautulli", var_type="url", default=self.general["tautulli"]["url"], req_default=True, save=False),
"apikey": check_for_attribute(lib, "apikey", parent="tautulli", default=self.general["tautulli"]["apikey"], req_default=True, save=False)
})
except Failed as e:
self.errors.append(e)
util.print_stacktrace()
util.print_multiline(e, error=True)
logger.info("")
logger.info(f"{display_name} library's Tautulli Connection {'Failed' if library.Tautulli is None else 'Successful'}")
if len(self.libraries) > 0:
logger.info(f"{len(self.libraries)} Plex Library Connection{'s' if len(self.libraries) > 1 else ''} Successful")
else:
raise Failed("Plex Error: No Plex libraries were connected to")
library.Notifiarr = self.NotifiarrFactory.getNotifiarr(library) if self.NotifiarrFactory else None
util.separator()
logger.info("")
self.libraries.append(library)
util.separator()
if len(self.libraries) > 0:
logger.info(f"{len(self.libraries)} Plex Library Connection{'s' if len(self.libraries) > 1 else ''} Successful")
else:
raise Failed("Plex Error: No Plex libraries were connected to")
util.separator()
if self.errors:
self.notify(self.errors)
except Exception as e:
self.notify(e)
raise
def notify(self, text, library=None, collection=None, critical=True):
if self.NotifiarrFactory:
if not isinstance(text, list):
text = [text]
for t in text:
self.NotifiarrFactory.error(t, library=library, collection=collection, critical=critical)
def get_html(self, url, headers=None, params=None):
return html.fromstring(self.get(url, headers=headers, params=params).content)
def get_json(self, url, headers=None):
return self.get(url, headers=headers).json()
def get_json(self, url, json=None, headers=None, params=None):
return self.get(url, json=json, headers=headers, params=params).json()
@retry(stop_max_attempt_number=6, wait_fixed=10000)
def get(self, url, headers=None, params=None):
return self.session.get(url, headers=headers, params=params)
def get(self, url, json=None, headers=None, params=None):
return self.session.get(url, json=json, headers=headers, params=params)
def get_image_encoded(self, url):
return base64.b64encode(self.get(url).content).decode('utf-8')
def post_html(self, url, data=None, json=None, headers=None):
return html.fromstring(self.post(url, data=data, json=json, headers=headers).content)

View file

@ -57,7 +57,7 @@ class IMDb:
pass
if total > 0:
return total, item_counts[page_type]
raise ValueError(f"IMDb Error: Failed to parse URL: {imdb_url}")
raise Failed(f"IMDb Error: Failed to parse URL: {imdb_url}")
def _ids_from_url(self, imdb_url, language, limit):
total, item_count = self._total(imdb_url, language)
@ -93,7 +93,7 @@ class IMDb:
if len(imdb_ids) > 0:
logger.debug(f"{len(imdb_ids)} IMDb IDs Found: {imdb_ids}")
return imdb_ids
raise ValueError(f"IMDb Error: No IMDb IDs Found at {imdb_url}")
raise Failed(f"IMDb Error: No IMDb IDs Found at {imdb_url}")
def get_imdb_ids(self, method, data, language):
if method == "imdb_id":

View file

@ -13,6 +13,7 @@ class Library(ABC):
self.Radarr = None
self.Sonarr = None
self.Tautulli = None
self.Notifiarr = None
self.collections = []
self.metadatas = []
self.metadata_files = []
@ -54,6 +55,9 @@ class Library(ABC):
self.sonarr_add_all = params["sonarr_add_all"]
self.collection_minimum = params["collection_minimum"]
self.delete_below_minimum = params["delete_below_minimum"]
self.notifiarr_collection_creation = params["notifiarr_collection_creation"]
self.notifiarr_collection_addition = params["notifiarr_collection_addition"]
self.notifiarr_collection_removing = params["notifiarr_collection_removing"]
self.split_duplicates = params["split_duplicates"] # TODO: Here or just in Plex?
self.clean_bundles = params["plex"]["clean_bundles"] # TODO: Here or just in Plex?
self.empty_trash = params["plex"]["empty_trash"] # TODO: Here or just in Plex?
@ -175,6 +179,9 @@ class Library(ABC):
if background_uploaded:
self.config.Cache.update_image_map(item.ratingKey, f"{self.image_table_name}_backgrounds", item.art, background.compare)
def notify(self, text, collection=None, critical=True):
self.config.notify(text, library=self, collection=collection, critical=critical)
@abstractmethod
def _upload_image(self, item, image):
pass

View file

@ -249,6 +249,7 @@ class Metadata:
add_edit("originally_available", item, meta, methods, key="originallyAvailableAt", value=originally_available, var_type="date")
add_edit("critic_rating", item, meta, methods, value=rating, key="rating", var_type="float")
add_edit("audience_rating", item, meta, methods, key="audienceRating", var_type="float")
add_edit("user_rating", item, meta, methods, key="userRating", var_type="float")
add_edit("content_rating", item, meta, methods, key="contentRating")
add_edit("original_title", item, meta, methods, key="originalTitle", value=original_title)
add_edit("studio", item, meta, methods, value=studio)

76
modules/notifiarr.py Normal file
View file

@ -0,0 +1,76 @@
import logging
from modules.util import Failed
logger = logging.getLogger("Plex Meta Manager")
base_url = "https://notifiarr.com/api/v1/"
dev_url = "https://dev.notifiarr.com/api/v1/"
class NotifiarrBase:
def __init__(self, config, apikey, develop, test, error_notification):
self.config = config
self.apikey = apikey
self.develop = develop
self.test = test
self.error_notification = error_notification
def _request(self, path, json=None, params=None):
url = f"{dev_url if self.develop else base_url}" + \
("notification/test" if self.test else f"{path}{self.apikey}")
logger.debug(url)
response = self.config.get(url, json=json, params={"event": "pmm"} if self.test else params)
response_json = response.json()
if self.develop or self.test:
logger.debug(json)
logger.debug("")
logger.debug(response_json)
if response.status_code >= 400 or ("response" in response_json and response_json["response"] == "error"):
raise Failed(f"({response.status_code} [{response.reason}]) {response_json}")
return response_json
def error(self, text, library=None, collection=None, critical=True):
if self.error_notification:
json = {"error": str(text), "critical": critical}
if library:
json["server_name"] = library.PlexServer.friendlyName
json["library_name"] = library.name
if collection:
json["collection"] = str(collection)
self._request("notification/plex/", json=json, params={"event": "collections"})
class NotifiarrFactory(NotifiarrBase):
def __init__(self, config, params):
super().__init__(config, params["apikey"], params["develop"], params["test"], params["error_notification"])
if not params["test"] and not self._request("user/validate/")["message"]["response"]:
raise Failed("Notifiarr Error: Invalid apikey")
def getNotifiarr(self, library):
return Notifiarr(self.config, library, self.apikey, self.develop, self.test, self.error_notification)
class Notifiarr(NotifiarrBase):
def __init__(self, config, library, apikey, develop, test, error_notification):
super().__init__(config, apikey, develop, test, error_notification)
self.library = library
def plex_collection(self, collection, created=False, additions=None, removals=None):
thumb = None
if collection.thumb and next((f for f in collection.fields if f.name == "thumb"), None):
thumb = self.config.get_image_encoded(f"{self.library.url}{collection.thumb}?X-Plex-Token={self.library.token}")
art = None
if collection.art and next((f for f in collection.fields if f.name == "art"), None):
art = self.config.get_image_encoded(f"{self.library.url}{collection.art}?X-Plex-Token={self.library.token}")
json = {
"server_name": self.library.PlexServer.friendlyName,
"library_name": self.library.name,
"type": "movie" if self.library.is_movie else "show",
"collection": collection.title,
"created": created,
"poster": thumb,
"background": art
}
if additions:
json["additions"] = additions
if removals:
json["removals"] = removals
self._request("notification/plex/", json=json, params={"event": "collections"})

View file

@ -56,9 +56,12 @@ class TMDb:
self.TMDb = tmdbv3api.TMDb(session=self.config.session)
self.TMDb.api_key = params["apikey"]
self.TMDb.language = params["language"]
response = tmdbv3api.Configuration().info()
if hasattr(response, "status_message"):
raise Failed(f"TMDb Error: {response.status_message}")
try:
response = tmdbv3api.Configuration().info()
if hasattr(response, "status_message"):
raise Failed(f"TMDb Error: {response.status_message}")
except TMDbException as e:
raise Failed(f"TMDb Error: {e}")
self.apikey = params["apikey"]
self.language = params["language"]
self.Movie = tmdbv3api.Movie()

View file

@ -29,6 +29,9 @@ class ImageData:
self.compare = location if is_url else os.stat(location).st_size
self.message = f"{prefix}{'poster' if is_poster else 'background'} to [{'URL' if is_url else 'File'}] {location}"
def __str__(self):
return str(self.__dict__)
def retry_if_not_failed(exception):
return not isinstance(exception, Failed)

View file

@ -98,7 +98,7 @@ logger.addHandler(cmd_handler)
sys.excepthook = util.my_except_hook
def start(config_path, is_test=False, time_scheduled=None, requested_collections=None, requested_libraries=None, resume_from=None):
def start(attrs):
file_logger = os.path.join(default_dir, "logs", "meta.log")
should_roll_over = os.path.isfile(file_logger)
file_handler = RotatingFileHandler(file_logger, delay=True, mode="w", backupCount=10, encoding="utf-8")
@ -115,107 +115,117 @@ def start(config_path, is_test=False, time_scheduled=None, requested_collections
logger.info(util.centered("| __/| | __/> < | | | | __/ || (_| | | | | | (_| | | | | (_| | (_| | __/ | "))
logger.info(util.centered("|_| |_|\\___/_/\\_\\ |_| |_|\\___|\\__\\__,_| |_| |_|\\__,_|_| |_|\\__,_|\\__, |\\___|_| "))
logger.info(util.centered(" |___/ "))
logger.info(util.centered(" Version: 1.12.2-develop0930 "))
if time_scheduled: start_type = f"{time_scheduled} "
elif is_test: start_type = "Test "
elif requested_collections: start_type = "Collections "
elif requested_libraries: start_type = "Libraries "
logger.info(util.centered(" Version: 1.12.2-develop1004 "))
if "time" in attrs: start_type = f"{attrs['time']} "
elif "test" in attrs: start_type = "Test "
elif "collections" in attrs: start_type = "Collections "
elif "libraries" in attrs: start_type = "Libraries "
else: start_type = ""
start_time = datetime.now()
if time_scheduled is None:
time_scheduled = start_time.strftime("%H:%M")
if "time" not in attrs:
attrs["time"] = start_time.strftime("%H:%M")
util.separator(f"Starting {start_type}Run")
try:
config = Config(default_dir, config_path=config_path, is_test=is_test,
time_scheduled=time_scheduled, requested_collections=requested_collections,
requested_libraries=requested_libraries, resume_from=resume_from)
update_libraries(config)
config = Config(default_dir, attrs)
except Exception as e:
util.print_stacktrace()
util.print_multiline(e, critical=True)
else:
try:
update_libraries(config)
except Exception as e:
config.notify(e)
util.print_stacktrace()
util.print_multiline(e, critical=True)
logger.info("")
util.separator(f"Finished {start_type}Run\nRun Time: {str(datetime.now() - start_time).split('.')[0]}")
logger.removeHandler(file_handler)
def update_libraries(config):
for library in config.libraries:
os.makedirs(os.path.join(default_dir, "logs", library.mapping_name, "collections"), exist_ok=True)
col_file_logger = os.path.join(default_dir, "logs", library.mapping_name, "library.log")
should_roll_over = os.path.isfile(col_file_logger)
library_handler = RotatingFileHandler(col_file_logger, delay=True, mode="w", backupCount=3, encoding="utf-8")
util.apply_formatter(library_handler)
if should_roll_over:
library_handler.doRollover()
logger.addHandler(library_handler)
try:
os.makedirs(os.path.join(default_dir, "logs", library.mapping_name, "collections"), exist_ok=True)
col_file_logger = os.path.join(default_dir, "logs", library.mapping_name, "library.log")
should_roll_over = os.path.isfile(col_file_logger)
library_handler = RotatingFileHandler(col_file_logger, delay=True, mode="w", backupCount=3, encoding="utf-8")
util.apply_formatter(library_handler)
if should_roll_over:
library_handler.doRollover()
logger.addHandler(library_handler)
os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(library.timeout)
logger.info("")
util.separator(f"{library.name} Library")
items = None
if not library.is_other:
os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(library.timeout)
logger.info("")
util.separator(f"Mapping {library.name} Library", space=False, border=False)
logger.info("")
items = library.map_guids()
if not config.test_mode and not config.resume_from and not collection_only and library.mass_update:
mass_metadata(config, library, items=items)
for metadata in library.metadata_files:
logger.info("")
util.separator(f"Running Metadata File\n{metadata.path}")
if not config.test_mode and not config.resume_from and not collection_only:
try:
metadata.update_metadata()
except Failed as e:
logger.error(e)
collections_to_run = metadata.get_collections(config.requested_collections)
if config.resume_from and config.resume_from not in collections_to_run:
util.separator(f"{library.name} Library")
items = None
if not library.is_other:
logger.info("")
logger.warning(f"Collection: {config.resume_from} not in Metadata File: {metadata.path}")
continue
if collections_to_run and not library_only:
util.separator(f"Mapping {library.name} Library", space=False, border=False)
logger.info("")
util.separator(f"{'Test ' if config.test_mode else ''}Collections")
logger.removeHandler(library_handler)
run_collection(config, library, metadata, collections_to_run)
logger.addHandler(library_handler)
if library.run_sort:
logger.info("")
util.separator(f"Sorting {library.name} Library's Collections", space=False, border=False)
logger.info("")
for builder in library.run_sort:
items = library.map_guids()
if not config.test_mode and not config.resume_from and not collection_only and library.mass_update:
mass_metadata(config, library, items=items)
for metadata in library.metadata_files:
logger.info("")
util.separator(f"Sorting {builder.name} Collection", space=False, border=False)
util.separator(f"Running Metadata File\n{metadata.path}")
if not config.test_mode and not config.resume_from and not collection_only:
try:
metadata.update_metadata()
except Failed as e:
library.notify(e)
logger.error(e)
collections_to_run = metadata.get_collections(config.requested_collections)
if config.resume_from and config.resume_from not in collections_to_run:
logger.info("")
logger.warning(f"Collection: {config.resume_from} not in Metadata File: {metadata.path}")
continue
if collections_to_run and not library_only:
logger.info("")
util.separator(f"{'Test ' if config.test_mode else ''}Collections")
logger.removeHandler(library_handler)
run_collection(config, library, metadata, collections_to_run)
logger.addHandler(library_handler)
if library.run_sort:
logger.info("")
builder.sort_collection()
util.separator(f"Sorting {library.name} Library's Collections", space=False, border=False)
logger.info("")
for builder in library.run_sort:
logger.info("")
util.separator(f"Sorting {builder.name} Collection", space=False, border=False)
logger.info("")
builder.sort_collection()
if not config.test_mode and not config.requested_collections and ((library.show_unmanaged and not library_only) or (library.assets_for_all and not collection_only)):
logger.info("")
util.separator(f"Other {library.name} Library Operations")
unmanaged_collections = []
for col in library.get_all_collections():
if col.title not in library.collections:
unmanaged_collections.append(col)
if not config.test_mode and not config.requested_collections and ((library.show_unmanaged and not library_only) or (library.assets_for_all and not collection_only)):
logger.info("")
util.separator(f"Other {library.name} Library Operations")
unmanaged_collections = []
for col in library.get_all_collections():
if col.title not in library.collections:
unmanaged_collections.append(col)
if library.show_unmanaged and not library_only:
logger.info("")
util.separator(f"Unmanaged Collections in {library.name} Library", space=False, border=False)
logger.info("")
for col in unmanaged_collections:
logger.info(col.title)
logger.info("")
logger.info(f"{len(unmanaged_collections)} Unmanaged Collections")
if library.show_unmanaged and not library_only:
logger.info("")
util.separator(f"Unmanaged Collections in {library.name} Library", space=False, border=False)
logger.info("")
for col in unmanaged_collections:
logger.info(col.title)
logger.info("")
logger.info(f"{len(unmanaged_collections)} Unmanaged Collections")
if library.assets_for_all and not collection_only:
logger.info("")
util.separator(f"All {library.type}s Assets Check for {library.name} Library", space=False, border=False)
logger.info("")
for col in unmanaged_collections:
poster, background = library.find_collection_assets(col, create=library.create_asset_folders)
library.upload_images(col, poster=poster, background=background)
for item in library.get_all():
library.update_item_from_assets(item, create=library.create_asset_folders)
if library.assets_for_all and not collection_only:
logger.info("")
util.separator(f"All {library.type}s Assets Check for {library.name} Library", space=False, border=False)
logger.info("")
for col in unmanaged_collections:
poster, background = library.find_collection_assets(col, create=library.create_asset_folders)
library.upload_images(col, poster=poster, background=background)
for item in library.get_all():
library.update_item_from_assets(item, create=library.create_asset_folders)
logger.removeHandler(library_handler)
logger.removeHandler(library_handler)
except Exception as e:
library.notify(e)
util.print_stacktrace()
util.print_multiline(e, critical=True)
has_run_again = False
for library in config.libraries:
@ -234,26 +244,32 @@ def update_libraries(config):
util.print_end()
for library in config.libraries:
if library.run_again:
col_file_logger = os.path.join(default_dir, "logs", library.mapping_name, f"library.log")
library_handler = RotatingFileHandler(col_file_logger, mode="w", backupCount=3, encoding="utf-8")
util.apply_formatter(library_handler)
logger.addHandler(library_handler)
library_handler.addFilter(fmt_filter)
os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(library.timeout)
logger.info("")
util.separator(f"{library.name} Library Run Again")
logger.info("")
library.map_guids()
for builder in library.run_again:
try:
col_file_logger = os.path.join(default_dir, "logs", library.mapping_name, f"library.log")
library_handler = RotatingFileHandler(col_file_logger, mode="w", backupCount=3, encoding="utf-8")
util.apply_formatter(library_handler)
logger.addHandler(library_handler)
library_handler.addFilter(fmt_filter)
os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(library.timeout)
logger.info("")
util.separator(f"{builder.name} Collection")
util.separator(f"{library.name} Library Run Again")
logger.info("")
try:
builder.run_collections_again()
except Failed as e:
util.print_stacktrace()
util.print_multiline(e, error=True)
logger.removeHandler(library_handler)
library.map_guids()
for builder in library.run_again:
logger.info("")
util.separator(f"{builder.name} Collection")
logger.info("")
try:
builder.run_collections_again()
except Failed as e:
library.notify(e, collection=builder.name, critical=False)
util.print_stacktrace()
util.print_multiline(e, error=True)
logger.removeHandler(library_handler)
except Exception as e:
library.notify(e)
util.print_stacktrace()
util.print_multiline(e, critical=True)
used_url = []
for library in config.libraries:
@ -457,7 +473,7 @@ def run_collection(config, library, metadata, requested_collections):
collection_log_name, output_str = util.validate_filename(mapping_name)
collection_log_folder = os.path.join(default_dir, "logs", library.mapping_name, "collections", collection_log_name)
os.makedirs(collection_log_folder, exist_ok=True)
col_file_logger = os.path.join(collection_log_folder, f"collection.log")
col_file_logger = os.path.join(collection_log_folder, "collection.log")
should_roll_over = os.path.isfile(col_file_logger)
collection_handler = RotatingFileHandler(col_file_logger, delay=True, mode="w", backupCount=3, encoding="utf-8")
util.apply_formatter(collection_handler)
@ -533,6 +549,8 @@ def run_collection(config, library, metadata, requested_collections):
library.run_sort.append(builder)
# builder.sort_collection()
builder.send_notifications()
if builder.item_details and run_item_details:
try:
builder.load_collection_items()
@ -546,9 +564,11 @@ def run_collection(config, library, metadata, requested_collections):
library.run_again.append(builder)
except Failed as e:
library.notify(e, collection=mapping_name)
util.print_stacktrace()
util.print_multiline(e, error=True)
except Exception as e:
library.notify(f"Unknown Error: {e}", collection=mapping_name)
util.print_stacktrace()
logger.error(f"Unknown Error: {e}")
logger.info("")
@ -557,7 +577,13 @@ def run_collection(config, library, metadata, requested_collections):
try:
if run or test or collections or libraries or resume:
start(config_file, is_test=test, requested_collections=collections, requested_libraries=libraries, resume_from=resume)
start({
"config_file": config_file,
"test": test,
"collections": collections,
"libraries": libraries,
"resume": resume
})
else:
times_to_run = util.get_list(times)
valid_times = []
@ -570,7 +596,7 @@ try:
else:
raise Failed(f"Argument Error: blank time argument")
for time_to_run in valid_times:
schedule.every().day.at(time_to_run).do(start, config_file, time_scheduled=time_to_run)
schedule.every().day.at(time_to_run).do(start, {"config_file": config_file, "time": time_to_run})
while True:
schedule.run_pending()
if not no_countdown: