push notifiarr update

pull/402/head
meisnate12 3 years ago
parent cef150bec0
commit 8516ac10db

@ -76,7 +76,10 @@ summary_details = [
] ]
poster_details = ["url_poster", "tmdb_poster", "tmdb_profile", "tvdb_poster", "file_poster"] poster_details = ["url_poster", "tmdb_poster", "tmdb_profile", "tvdb_poster", "file_poster"]
background_details = ["url_background", "tmdb_background", "tvdb_background", "file_background"] 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"] string_details = ["sort_title", "content_rating", "name_mapping"]
ignored_details = [ ignored_details = [
"smart_filter", "smart_label", "smart_url", "run_again", "schedule", "sync_mode", "template", "test", "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, "save_missing": self.library.save_missing,
"missing_only_released": self.library.missing_only_released, "missing_only_released": self.library.missing_only_released,
"create_asset_folders": self.library.create_asset_folders, "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.item_details = {}
self.radarr_details = {} self.radarr_details = {}
@ -183,6 +189,8 @@ class CollectionBuilder:
self.filtered_keys = {} self.filtered_keys = {}
self.run_again_movies = [] self.run_again_movies = []
self.run_again_shows = [] self.run_again_shows = []
self.notifiarr_additions = []
self.notifiarr_removals = []
self.items = [] self.items = []
self.posters = {} self.posters = {}
self.backgrounds = {} self.backgrounds = {}
@ -191,6 +199,8 @@ class CollectionBuilder:
self.minimum = self.library.collection_minimum self.minimum = self.library.collection_minimum
self.current_time = datetime.now() self.current_time = datetime.now()
self.current_year = self.current_time.year self.current_year = self.current_time.year
self.exists = False
self.created = False
methods = {m.lower(): m for m in self.data} 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.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.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.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_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 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") 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: if self.sync and self.obj:
for item in self.library.get_collection_items(self.obj, self.smart_label_collection): for item in self.library.get_collection_items(self.obj, self.smart_label_collection):
self.plex_map[item.ratingKey] = item self.plex_map[item.ratingKey] = item
if self.obj:
self.exists = True
else: else:
self.obj = None self.obj = None
self.sync = False self.sync = False
@ -1122,7 +1135,7 @@ class CollectionBuilder:
rating_keys.append(input_id) rating_keys.append(input_id)
elif id_type == "tmdb" and not self.parts_collection: elif id_type == "tmdb" and not self.parts_collection:
if input_id in self.library.movie_map: 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: elif input_id not in self.missing_movies:
self.missing_movies.append(input_id) self.missing_movies.append(input_id)
elif id_type in ["tvdb", "tmdb_show"] and not self.parts_collection: elif id_type in ["tvdb", "tmdb_show"] and not self.parts_collection:
@ -1133,12 +1146,12 @@ class CollectionBuilder:
logger.error(e) logger.error(e)
continue continue
if input_id in self.library.show_map: 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: elif input_id not in self.missing_shows:
self.missing_shows.append(input_id) self.missing_shows.append(input_id)
elif id_type == "imdb" and not self.parts_collection: elif id_type == "imdb" and not self.parts_collection:
if input_id in self.library.imdb_map: 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: else:
if self.do_missing: if self.do_missing:
try: try:
@ -1486,6 +1499,14 @@ class CollectionBuilder:
self.plex_map[current.ratingKey] = None self.plex_map[current.ratingKey] = None
else: else:
self.library.alter_collection(current, name, smart_label_collection=self.smart_label_collection) 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() util.print_end()
logger.info("") logger.info("")
logger.info(f"{total} {self.collection_level.capitalize()}{'s' if total > 1 else ''} Processed") logger.info(f"{total} {self.collection_level.capitalize()}{'s' if total > 1 else ''} Processed")
@ -1714,6 +1735,14 @@ class CollectionBuilder:
self.library.reload(item) self.library.reload(item)
logger.info(f"{self.name} Collection | - | {self.item_title(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) 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 count_removed += 1
if count_removed > 0: if count_removed > 0:
logger.info("") logger.info("")
@ -1835,6 +1864,8 @@ class CollectionBuilder:
except Failed: except Failed:
raise Failed(f"Collection Error: Label: {self.name} was not added to any items in the Library") 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) self.obj = self.library.get_collection(self.name)
if not self.exists:
self.created = True
def update_details(self): def update_details(self):
logger.info("") logger.info("")
@ -2002,10 +2033,26 @@ class CollectionBuilder:
self.library.move_item(self.obj, key, after=previous) self.library.move_item(self.obj, key, after=previous)
previous = key 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): def run_collections_again(self):
self.obj = self.library.get_collection(self.name) self.obj = self.library.get_collection(self.name)
name, collection_items = self.library.get_collection_name_and_items(self.obj, self.smart_label_collection) name, collection_items = self.library.get_collection_name_and_items(self.obj, self.smart_label_collection)
self.created = False
rating_keys = [] rating_keys = []
self.notifiarr_additions = []
for mm in self.run_again_movies: for mm in self.run_again_movies:
if mm in self.library.movie_map: if mm in self.library.movie_map:
rating_keys.extend(self.library.movie_map[mm]) rating_keys.extend(self.library.movie_map[mm])
@ -2025,6 +2072,14 @@ class CollectionBuilder:
else: else:
self.library.alter_collection(current, name, smart_label_collection=self.smart_label_collection) self.library.alter_collection(current, name, smart_label_collection=self.smart_label_collection)
logger.info(f"{name} Collection | + | {self.item_title(current)}") 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") 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: if len(self.run_again_movies) > 0:

@ -1,4 +1,4 @@
import logging, os, requests import base64, logging, os, requests
from datetime import datetime from datetime import datetime
from lxml import html from lxml import html
from modules import util, radarr, sonarr from modules import util, radarr, sonarr
@ -10,6 +10,7 @@ from modules.icheckmovies import ICheckMovies
from modules.imdb import IMDb from modules.imdb import IMDb
from modules.letterboxd import Letterboxd from modules.letterboxd import Letterboxd
from modules.mal import MyAnimeList from modules.mal import MyAnimeList
from modules.notifiarr import NotifiarrFactory
from modules.omdb import OMDb from modules.omdb import OMDb
from modules.plex import Plex from modules.plex import Plex
from modules.radarr import Radarr 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"} mass_update_options = {"tmdb": "Use TMDb Metadata", "omdb": "Use IMDb Metadata through OMDb"}
class Config: 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...") logger.info("Locating config...")
if config_path and os.path.exists(config_path): self.config_path = os.path.abspath(config_path) config_file = attrs["config_file"]
elif config_path and not os.path.exists(config_path): raise Failed(f"Config Error: config not found at {os.path.abspath(config_path)}") 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")) 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)}") else: raise Failed(f"Config Error: config not found at {os.path.abspath(default_dir)}")
logger.info(f"Using {self.config_path} as config") logger.info(f"Using {self.config_path} as config")
self.default_dir = default_dir self.default_dir = default_dir
self.test_mode = is_test self.test_mode = attrs["test"]
self.run_start_time = time_scheduled self.run_start_time = attrs["time"]
self.run_hour = datetime.strptime(time_scheduled, "%H:%M").hour self.run_hour = datetime.strptime(attrs["time"], "%H:%M").hour
self.requested_collections = util.get_list(requested_collections) self.requested_collections = util.get_list(attrs["collections"])
self.requested_libraries = util.get_list(requested_libraries) self.requested_libraries = util.get_list(attrs["libraries"])
self.resume_from = resume_from self.resume_from = attrs["resume"]
yaml.YAML().allow_duplicate_keys = True yaml.YAML().allow_duplicate_keys = True
try: try:
@ -87,6 +89,7 @@ class Config:
if "radarr" in new_config: new_config["radarr"] = new_config.pop("radarr") 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 "sonarr" in new_config: new_config["sonarr"] = new_config.pop("sonarr")
if "omdb" in new_config: new_config["omdb"] = new_config.pop("omdb") 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 "trakt" in new_config: new_config["trakt"] = new_config.pop("trakt")
if "mal" in new_config: new_config["mal"] = new_config.pop("mal") if "mal" in new_config: new_config["mal"] = new_config.pop("mal")
if "anidb" in new_config: new_config["anidb"] = new_config.pop("anidb") 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), "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), "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), "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"]: if self.general["cache"]:
util.separator() util.separator()
@ -196,6 +202,27 @@ class Config:
util.separator() util.separator()
self.NotifiarrFactory = None
if "notifiarr" in self.data:
logger.info("Connecting to Notifiarr...")
try:
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"Notifiarr Connection {'Failed' if self.NotifiarrFactory is None else 'Successful'}")
else:
logger.warning("notifiarr attribute not found")
self.errors = []
util.separator()
try:
self.TMDb = None self.TMDb = None
if "tmdb" in self.data: if "tmdb" in self.data:
logger.info("Connecting to TMDb...") logger.info("Connecting to TMDb...")
@ -215,6 +242,7 @@ class Config:
try: try:
self.OMDb = OMDb(self, {"apikey": check_for_attribute(self.data, "apikey", parent="omdb", throw=True)}) self.OMDb = OMDb(self, {"apikey": check_for_attribute(self.data, "apikey", parent="omdb", throw=True)})
except Failed as e: except Failed as e:
self.errors.append(e)
logger.error(e) logger.error(e)
logger.info(f"OMDb Connection {'Failed' if self.OMDb is None else 'Successful'}") logger.info(f"OMDb Connection {'Failed' if self.OMDb is None else 'Successful'}")
else: else:
@ -233,6 +261,7 @@ class Config:
"authorization": self.data["trakt"]["authorization"] if "authorization" in self.data["trakt"] else None "authorization": self.data["trakt"]["authorization"] if "authorization" in self.data["trakt"] else None
}) })
except Failed as e: except Failed as e:
self.errors.append(e)
logger.error(e) logger.error(e)
logger.info(f"Trakt Connection {'Failed' if self.Trakt is None else 'Successful'}") logger.info(f"Trakt Connection {'Failed' if self.Trakt is None else 'Successful'}")
else: else:
@ -251,6 +280,7 @@ class Config:
"authorization": self.data["mal"]["authorization"] if "authorization" in self.data["mal"] else None "authorization": self.data["mal"]["authorization"] if "authorization" in self.data["mal"] else None
}) })
except Failed as e: except Failed as e:
self.errors.append(e)
logger.error(e) logger.error(e)
logger.info(f"My Anime List Connection {'Failed' if self.MyAnimeList is None else 'Successful'}") logger.info(f"My Anime List Connection {'Failed' if self.MyAnimeList is None else 'Successful'}")
else: else:
@ -268,6 +298,7 @@ class Config:
"password": check_for_attribute(self.data, "password", parent="anidb", throw=True) "password": check_for_attribute(self.data, "password", parent="anidb", throw=True)
}) })
except Failed as e: except Failed as e:
self.errors.append(e)
logger.error(e) logger.error(e)
logger.info(f"My Anime List Connection {'Failed Continuing as Guest ' if self.MyAnimeList is None else 'Successful'}") logger.info(f"My Anime List Connection {'Failed Continuing as Guest ' if self.MyAnimeList is None else 'Successful'}")
if self.AniDB is None: if self.AniDB is None:
@ -357,26 +388,37 @@ class Config:
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["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["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["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) 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": if self.OMDb is None and params["mass_genre_update"] == "omdb":
params["mass_genre_update"] = None params["mass_genre_update"] = None
logger.error("Config Error: mass_genre_update cannot be omdb without a successful OMDb Connection") 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) 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": if self.OMDb is None and params["mass_audience_rating_update"] == "omdb":
params["mass_audience_rating_update"] = None params["mass_audience_rating_update"] = None
logger.error("Config Error: mass_audience_rating_update cannot be omdb without a successful OMDb Connection") 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) 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": if self.OMDb is None and params["mass_critic_rating_update"] == "omdb":
params["mass_critic_rating_update"] = None params["mass_critic_rating_update"] = None
logger.error("Config Error: mass_critic_rating_update cannot be omdb without a successful OMDb Connection") 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) 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"]: if self.Trakt is None and params["mass_trakt_rating_update"]:
params["mass_trakt_rating_update"] = None params["mass_trakt_rating_update"] = None
logger.error("Config Error: mass_trakt_rating_update cannot run without a successful Trakt Connection") 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["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["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)
@ -393,7 +435,9 @@ class Config:
def check_dict(attr, name): def check_dict(attr, name):
if attr in path: if attr in path:
if path[attr] is None: if path[attr] is None:
logger.error(f"Config Error: metadata_path {attr} is blank") e = f"Config Error: metadata_path {attr} is blank"
self.errors.append(e)
logger.error(e)
else: else:
params["metadata_path"].append((name, path[attr])) params["metadata_path"].append((name, path[attr]))
check_dict("url", "URL") check_dict("url", "URL")
@ -417,6 +461,7 @@ class Config:
logger.info("") logger.info("")
logger.info(f"{display_name} Library Connection Successful") logger.info(f"{display_name} Library Connection Successful")
except Failed as e: except Failed as e:
self.errors.append(e)
util.print_stacktrace() util.print_stacktrace()
util.print_multiline(e, error=True) util.print_multiline(e, error=True)
logger.info(f"{display_name} Library Connection Failed") logger.info(f"{display_name} Library Connection Failed")
@ -442,6 +487,7 @@ class Config:
"search": check_for_attribute(lib, "search", parent="radarr", var_type="bool", default=self.general["radarr"]["search"], save=False) "search": check_for_attribute(lib, "search", parent="radarr", var_type="bool", default=self.general["radarr"]["search"], save=False)
}) })
except Failed as e: except Failed as e:
self.errors.append(e)
util.print_stacktrace() util.print_stacktrace()
util.print_multiline(e, error=True) util.print_multiline(e, error=True)
logger.info("") logger.info("")
@ -470,6 +516,7 @@ class Config:
"cutoff_search": check_for_attribute(lib, "cutoff_search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["cutoff_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: except Failed as e:
self.errors.append(e)
util.print_stacktrace() util.print_stacktrace()
util.print_multiline(e, error=True) util.print_multiline(e, error=True)
logger.info("") logger.info("")
@ -487,11 +534,14 @@ class Config:
"apikey": check_for_attribute(lib, "apikey", parent="tautulli", default=self.general["tautulli"]["apikey"], 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: except Failed as e:
self.errors.append(e)
util.print_stacktrace() util.print_stacktrace()
util.print_multiline(e, error=True) util.print_multiline(e, error=True)
logger.info("") logger.info("")
logger.info(f"{display_name} library's Tautulli Connection {'Failed' if library.Tautulli is None else 'Successful'}") logger.info(f"{display_name} library's Tautulli Connection {'Failed' if library.Tautulli is None else 'Successful'}")
library.Notifiarr = self.NotifiarrFactory.getNotifiarr(library) if self.NotifiarrFactory else None
logger.info("") logger.info("")
self.libraries.append(library) self.libraries.append(library)
@ -504,15 +554,31 @@ class Config:
util.separator() 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): def get_html(self, url, headers=None, params=None):
return html.fromstring(self.get(url, headers=headers, params=params).content) return html.fromstring(self.get(url, headers=headers, params=params).content)
def get_json(self, url, headers=None): def get_json(self, url, json=None, headers=None, params=None):
return self.get(url, headers=headers).json() return self.get(url, json=json, headers=headers, params=params).json()
@retry(stop_max_attempt_number=6, wait_fixed=10000) @retry(stop_max_attempt_number=6, wait_fixed=10000)
def get(self, url, headers=None, params=None): def get(self, url, json=None, headers=None, params=None):
return self.session.get(url, headers=headers, params=params) 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): def post_html(self, url, data=None, json=None, headers=None):
return html.fromstring(self.post(url, data=data, json=json, headers=headers).content) return html.fromstring(self.post(url, data=data, json=json, headers=headers).content)

@ -57,7 +57,7 @@ class IMDb:
pass pass
if total > 0: if total > 0:
return total, item_counts[page_type] 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): def _ids_from_url(self, imdb_url, language, limit):
total, item_count = self._total(imdb_url, language) total, item_count = self._total(imdb_url, language)
@ -93,7 +93,7 @@ class IMDb:
if len(imdb_ids) > 0: if len(imdb_ids) > 0:
logger.debug(f"{len(imdb_ids)} IMDb IDs Found: {imdb_ids}") logger.debug(f"{len(imdb_ids)} IMDb IDs Found: {imdb_ids}")
return 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): def get_imdb_ids(self, method, data, language):
if method == "imdb_id": if method == "imdb_id":

@ -13,6 +13,7 @@ class Library(ABC):
self.Radarr = None self.Radarr = None
self.Sonarr = None self.Sonarr = None
self.Tautulli = None self.Tautulli = None
self.Notifiarr = None
self.collections = [] self.collections = []
self.metadatas = [] self.metadatas = []
self.metadata_files = [] self.metadata_files = []
@ -54,6 +55,9 @@ class Library(ABC):
self.sonarr_add_all = params["sonarr_add_all"] self.sonarr_add_all = params["sonarr_add_all"]
self.collection_minimum = params["collection_minimum"] self.collection_minimum = params["collection_minimum"]
self.delete_below_minimum = params["delete_below_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.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.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? self.empty_trash = params["plex"]["empty_trash"] # TODO: Here or just in Plex?
@ -175,6 +179,9 @@ class Library(ABC):
if background_uploaded: if background_uploaded:
self.config.Cache.update_image_map(item.ratingKey, f"{self.image_table_name}_backgrounds", item.art, background.compare) 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 @abstractmethod
def _upload_image(self, item, image): def _upload_image(self, item, image):
pass pass

@ -249,6 +249,7 @@ class Metadata:
add_edit("originally_available", item, meta, methods, key="originallyAvailableAt", value=originally_available, var_type="date") 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("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("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("content_rating", item, meta, methods, key="contentRating")
add_edit("original_title", item, meta, methods, key="originalTitle", value=original_title) add_edit("original_title", item, meta, methods, key="originalTitle", value=original_title)
add_edit("studio", item, meta, methods, value=studio) add_edit("studio", item, meta, methods, value=studio)

@ -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"})

@ -56,9 +56,12 @@ class TMDb:
self.TMDb = tmdbv3api.TMDb(session=self.config.session) self.TMDb = tmdbv3api.TMDb(session=self.config.session)
self.TMDb.api_key = params["apikey"] self.TMDb.api_key = params["apikey"]
self.TMDb.language = params["language"] self.TMDb.language = params["language"]
try:
response = tmdbv3api.Configuration().info() response = tmdbv3api.Configuration().info()
if hasattr(response, "status_message"): if hasattr(response, "status_message"):
raise Failed(f"TMDb Error: {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.apikey = params["apikey"]
self.language = params["language"] self.language = params["language"]
self.Movie = tmdbv3api.Movie() self.Movie = tmdbv3api.Movie()

@ -29,6 +29,9 @@ class ImageData:
self.compare = location if is_url else os.stat(location).st_size 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}" 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): def retry_if_not_failed(exception):
return not isinstance(exception, Failed) return not isinstance(exception, Failed)

@ -98,7 +98,7 @@ logger.addHandler(cmd_handler)
sys.excepthook = util.my_except_hook 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") file_logger = os.path.join(default_dir, "logs", "meta.log")
should_roll_over = os.path.isfile(file_logger) should_roll_over = os.path.isfile(file_logger)
file_handler = RotatingFileHandler(file_logger, delay=True, mode="w", backupCount=10, encoding="utf-8") file_handler = RotatingFileHandler(file_logger, delay=True, mode="w", backupCount=10, encoding="utf-8")
@ -115,22 +115,26 @@ 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("|_| |_|\\___/_/\\_\\ |_| |_|\\___|\\__\\__,_| |_| |_|\\__,_|_| |_|\\__,_|\\__, |\\___|_| "))
logger.info(util.centered(" |___/ ")) logger.info(util.centered(" |___/ "))
logger.info(util.centered(" Version: 1.12.2-develop0930 ")) logger.info(util.centered(" Version: 1.12.2-develop1004 "))
if time_scheduled: start_type = f"{time_scheduled} " if "time" in attrs: start_type = f"{attrs['time']} "
elif is_test: start_type = "Test " elif "test" in attrs: start_type = "Test "
elif requested_collections: start_type = "Collections " elif "collections" in attrs: start_type = "Collections "
elif requested_libraries: start_type = "Libraries " elif "libraries" in attrs: start_type = "Libraries "
else: start_type = "" else: start_type = ""
start_time = datetime.now() start_time = datetime.now()
if time_scheduled is None: if "time" not in attrs:
time_scheduled = start_time.strftime("%H:%M") attrs["time"] = start_time.strftime("%H:%M")
util.separator(f"Starting {start_type}Run") util.separator(f"Starting {start_type}Run")
try: try:
config = Config(default_dir, config_path=config_path, is_test=is_test, config = Config(default_dir, attrs)
time_scheduled=time_scheduled, requested_collections=requested_collections, except Exception as e:
requested_libraries=requested_libraries, resume_from=resume_from) util.print_stacktrace()
util.print_multiline(e, critical=True)
else:
try:
update_libraries(config) update_libraries(config)
except Exception as e: except Exception as e:
config.notify(e)
util.print_stacktrace() util.print_stacktrace()
util.print_multiline(e, critical=True) util.print_multiline(e, critical=True)
logger.info("") logger.info("")
@ -139,6 +143,7 @@ def start(config_path, is_test=False, time_scheduled=None, requested_collections
def update_libraries(config): def update_libraries(config):
for library in config.libraries: for library in config.libraries:
try:
os.makedirs(os.path.join(default_dir, "logs", library.mapping_name, "collections"), exist_ok=True) 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") col_file_logger = os.path.join(default_dir, "logs", library.mapping_name, "library.log")
should_roll_over = os.path.isfile(col_file_logger) should_roll_over = os.path.isfile(col_file_logger)
@ -166,6 +171,7 @@ def update_libraries(config):
try: try:
metadata.update_metadata() metadata.update_metadata()
except Failed as e: except Failed as e:
library.notify(e)
logger.error(e) logger.error(e)
collections_to_run = metadata.get_collections(config.requested_collections) collections_to_run = metadata.get_collections(config.requested_collections)
if config.resume_from and config.resume_from not in collections_to_run: if config.resume_from and config.resume_from not in collections_to_run:
@ -216,6 +222,10 @@ def update_libraries(config):
library.update_item_from_assets(item, create=library.create_asset_folders) 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 has_run_again = False
for library in config.libraries: for library in config.libraries:
@ -234,6 +244,7 @@ def update_libraries(config):
util.print_end() util.print_end()
for library in config.libraries: for library in config.libraries:
if library.run_again: if library.run_again:
try:
col_file_logger = os.path.join(default_dir, "logs", library.mapping_name, f"library.log") 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") library_handler = RotatingFileHandler(col_file_logger, mode="w", backupCount=3, encoding="utf-8")
util.apply_formatter(library_handler) util.apply_formatter(library_handler)
@ -251,9 +262,14 @@ def update_libraries(config):
try: try:
builder.run_collections_again() builder.run_collections_again()
except Failed as e: except Failed as e:
library.notify(e, collection=builder.name, critical=False)
util.print_stacktrace() util.print_stacktrace()
util.print_multiline(e, error=True) util.print_multiline(e, error=True)
logger.removeHandler(library_handler) logger.removeHandler(library_handler)
except Exception as e:
library.notify(e)
util.print_stacktrace()
util.print_multiline(e, critical=True)
used_url = [] used_url = []
for library in config.libraries: 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_name, output_str = util.validate_filename(mapping_name)
collection_log_folder = os.path.join(default_dir, "logs", library.mapping_name, "collections", collection_log_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) 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) should_roll_over = os.path.isfile(col_file_logger)
collection_handler = RotatingFileHandler(col_file_logger, delay=True, mode="w", backupCount=3, encoding="utf-8") collection_handler = RotatingFileHandler(col_file_logger, delay=True, mode="w", backupCount=3, encoding="utf-8")
util.apply_formatter(collection_handler) util.apply_formatter(collection_handler)
@ -533,6 +549,8 @@ def run_collection(config, library, metadata, requested_collections):
library.run_sort.append(builder) library.run_sort.append(builder)
# builder.sort_collection() # builder.sort_collection()
builder.send_notifications()
if builder.item_details and run_item_details: if builder.item_details and run_item_details:
try: try:
builder.load_collection_items() builder.load_collection_items()
@ -546,9 +564,11 @@ def run_collection(config, library, metadata, requested_collections):
library.run_again.append(builder) library.run_again.append(builder)
except Failed as e: except Failed as e:
library.notify(e, collection=mapping_name)
util.print_stacktrace() util.print_stacktrace()
util.print_multiline(e, error=True) util.print_multiline(e, error=True)
except Exception as e: except Exception as e:
library.notify(f"Unknown Error: {e}", collection=mapping_name)
util.print_stacktrace() util.print_stacktrace()
logger.error(f"Unknown Error: {e}") logger.error(f"Unknown Error: {e}")
logger.info("") logger.info("")
@ -557,7 +577,13 @@ def run_collection(config, library, metadata, requested_collections):
try: try:
if run or test or collections or libraries or resume: 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: else:
times_to_run = util.get_list(times) times_to_run = util.get_list(times)
valid_times = [] valid_times = []
@ -570,7 +596,7 @@ try:
else: else:
raise Failed(f"Argument Error: blank time argument") raise Failed(f"Argument Error: blank time argument")
for time_to_run in valid_times: 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: while True:
schedule.run_pending() schedule.run_pending()
if not no_countdown: if not no_countdown:

Loading…
Cancel
Save