From 33b92f2a2d35d82972dd40df5d90a2a9199e8d67 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Fri, 26 Jan 2024 12:16:10 -0500 Subject: [PATCH] [21] Fix Gotify PR --- CHANGELOG | 2 +- VERSION | 2 +- config/config.yml.template | 2 +- docs/config/gotify.md | 10 +-- docs/config/webhooks.md | 4 +- json-schema/config-schema.json | 4 +- json-schema/prototype_config.yml | 2 +- modules/config.py | 30 ++------- modules/gotify.py | 107 +++++++++++++++++++++++-------- modules/library.py | 2 - modules/webhooks.py | 85 ++++-------------------- plex_meta_manager.py | 8 +-- 12 files changed, 116 insertions(+), 142 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index c25362cf..096ebe2c 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -6,7 +6,7 @@ Updated python-dotenv requirement to 1.0.1 # New Features Added `monitor_existing` to sonarr and radarr. To update the monitored status of items existing in plex to match the `monitor` declared. - +Added [Gotify](https://gotify.net/) as a notification service. Thanks @krstn420 for the initial code. # Updates Added new [BoxOfficeMojo Builder](https://metamanager.wiki/en/latest/files/builders/mojo/) - credit to @nwithan8 for the suggestion and initial code submission diff --git a/VERSION b/VERSION index 516a2bd2..ce9352a0 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.20.0-develop20 +1.20.0-develop21 diff --git a/config/config.yml.template b/config/config.yml.template index 5b43a781..86590520 100644 --- a/config/config.yml.template +++ b/config/config.yml.template @@ -107,7 +107,7 @@ notifiarr: apikey: #################################### gotify: url: http://192.168.1.12:80 - apikey: #################################### + token: #################################### anidb: # Not required for AniDB builders unless you want mature content username: ###### password: ###### diff --git a/docs/config/gotify.md b/docs/config/gotify.md index 0eac99dd..df4e621c 100644 --- a/docs/config/gotify.md +++ b/docs/config/gotify.md @@ -10,13 +10,13 @@ Below is a `gotify` mapping example and the full set of attributes: ```yaml gotify: url: #################################### - apikey: #################################### + token: #################################### ``` -| Attribute | Allowed Values | Required | -|:----------|:-----------------------------------------|:------------------------------------------:| -| `url` | Gotify Server Url | :fontawesome-solid-circle-check:{ .green } | -| `apikey` | Gotify Application API Key | :fontawesome-solid-circle-check:{ .green } | +| Attribute | Allowed Values | Required | +|:----------|:-------------------------|:------------------------------------------:| +| `url` | Gotify Server Url | :fontawesome-solid-circle-check:{ .green } | +| `token` | Gotify Application Token | :fontawesome-solid-circle-check:{ .green } | Once you have added the apikey your config.yml you have to add `gotify` to any [webhook](webhooks.md) to send that notification to Gotify. diff --git a/docs/config/webhooks.md b/docs/config/webhooks.md index 6f58ea07..30a22820 100644 --- a/docs/config/webhooks.md +++ b/docs/config/webhooks.md @@ -27,7 +27,8 @@ webhooks: | [`changes`](#changes-notifications) | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | :fontawesome-solid-circle-check:{ .green } | * Each Attribute can be either a webhook url as a string or a comma-separated list of webhooks urls. -* To send notifications to [Notifiarr](notifiarr.md) or [Gotify](gotify.md) just add `notifiarr` or `gotify` to a webhook instead of the webhook url. +* To send notifications to [Notifiarr](notifiarr.md) just add `notifiarr` to a webhook instead of the webhook url. +* To send notifications to [Gotify](gotify.md) just add `gotify` to a webhook instead of the webhook url. ## Error Notifications @@ -77,7 +78,6 @@ level the error occurs. "error": str, // Error Message "critical": bool, // Critical Error "server_name": str, // Server Name - "library_name": str, // Library Name "playlist": str // Playlist Name } ``` diff --git a/json-schema/config-schema.json b/json-schema/config-schema.json index a681f84f..cd8767db 100644 --- a/json-schema/config-schema.json +++ b/json-schema/config-schema.json @@ -293,13 +293,13 @@ "url": { "type": "string" }, - "apikey": { + "token": { "type": "string" } }, "required": [ "url", - "apikey" + "token" ], "title": "gotify" diff --git a/json-schema/prototype_config.yml b/json-schema/prototype_config.yml index 60d3ec64..74f09229 100644 --- a/json-schema/prototype_config.yml +++ b/json-schema/prototype_config.yml @@ -474,7 +474,7 @@ notifiarr: apikey: this-is-a-placeholder-string gotify: url: http://192.168.1.12:80 - apikey: this-is-a-placeholder-string + token: this-is-a-placeholder-string anidb: # Not required for AniDB builders unless you want mature content username: this-is-a-placeholder-string password: this-is-a-placeholder-string diff --git a/modules/config.py b/modules/config.py index 22066607..c6efd30d 100644 --- a/modules/config.py +++ b/modules/config.py @@ -528,32 +528,14 @@ class ConfigFile: else: logger.info("notifiarr attribute not found") - self.webhooks = { - "error": check_for_attribute(self.data, "error", parent="webhooks", var_type="list", default_is_none=True), - "version": check_for_attribute(self.data, "version", parent="webhooks", var_type="list", default_is_none=True), - "run_start": check_for_attribute(self.data, "run_start", parent="webhooks", var_type="list", default_is_none=True), - "run_end": check_for_attribute(self.data, "run_end", parent="webhooks", var_type="list", default_is_none=True), - "changes": check_for_attribute(self.data, "changes", parent="webhooks", var_type="list", default_is_none=True), - "delete": check_for_attribute(self.data, "delete", parent="webhooks", var_type="list", default_is_none=True) - } - self.Webhooks = Webhooks(self, self.webhooks, notifiarr=self.NotifiarrFactory) - try: - self.Webhooks.start_time_hooks(self.start_time) - if self.version[0] != "Unknown" and self.latest_version[0] != "Unknown" and self.version[1] != self.latest_version[1] or (self.version[2] and self.version[2] < self.latest_version[2]): - self.Webhooks.version_hooks(self.version, self.latest_version) - except Failed as e: - logger.stacktrace() - logger.error(f"Webhooks Error: {e}") - - logger.save_errors = True - logger.separator() - self.GotifyFactory = None if "gotify" in self.data: logger.info("Connecting to Gotify...") try: - self.GotifyFactory = Gotify(self, {"url": check_for_attribute(self.data, "url", parent="gotify", throw=True), - "apikey": check_for_attribute(self.data, "apikey", parent="gotify", throw=True)}) + self.GotifyFactory = Gotify(self, { + "url": check_for_attribute(self.data, "url", parent="gotify", throw=True), + "token": check_for_attribute(self.data, "token", parent="gotify", throw=True) + }) except Failed as e: if str(e).endswith("is blank"): logger.warning(e) @@ -572,7 +554,7 @@ class ConfigFile: "changes": check_for_attribute(self.data, "changes", parent="webhooks", var_type="list", default_is_none=True), "delete": check_for_attribute(self.data, "delete", parent="webhooks", var_type="list", default_is_none=True) } - self.Webhooks = Webhooks(self, self.webhooks, notifiarr=self.GotifyFactory) + self.Webhooks = Webhooks(self, self.webhooks, notifiarr=self.NotifiarrFactory, gotify=self.GotifyFactory) try: self.Webhooks.start_time_hooks(self.start_time) if self.version[0] != "Unknown" and self.latest_version[0] != "Unknown" and self.version[1] != self.latest_version[1] or (self.version[2] and self.version[2] < self.latest_version[2]): @@ -1243,7 +1225,7 @@ class ConfigFile: logger.info("") logger.info(f"{display_name} library's Tautulli Connection {'Failed' if library.Tautulli is None else 'Successful'}") - library.Webhooks = Webhooks(self, {}, library=library, notifiarr=self.NotifiarrFactory or self.GotifyFactory,) + library.Webhooks = Webhooks(self, {}, library=library, notifiarr=self.NotifiarrFactory, gotify=self.GotifyFactory) library.Overlays = Overlays(self, library) logger.info("") diff --git a/modules/gotify.py b/modules/gotify.py index 0bca93fb..db71d075 100644 --- a/modules/gotify.py +++ b/modules/gotify.py @@ -1,42 +1,99 @@ from json import JSONDecodeError from modules import util from modules.util import Failed -from retrying import retry logger = util.logger class Gotify: def __init__(self, config, params): self.config = config - self.apikey = params["apikey"] - self.url = params["url"] - logger.secret(self.apikey) + self.token = params["token"] + self.url = params["url"].rstrip("/") + logger.secret(self.url) + logger.secret(self.token) try: - self.request(path="message") - except JSONDecodeError: - raise Failed("Gotify Error: Invalid JSON response received") + logger.info(f"Gotify Version: {self._request(path='version', post=False)['version']}") + except Exception: + logger.stacktrace() + raise Failed("Gotify Error: Invalid URL") - def notification(self, json): - return self.request(json=json) - - @retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed) - def request(self, json=None, path="message"): - if not json: - json = { - "message": "Well hello there.", - "priority": 1, - "title": "This is first contact" - } - response = self.config.post(f"{self.url}{path}?token={self.apikey}", json=json) + def _request(self, path="message", json=None, post=True): + if post: + response = self.config.post(f"{self.url}/{path}", headers={"X-Gotify-Key": self.token}, json=json) + else: + response = self.config.get(f"{self.url}/{path}") try: response_json = response.json() except JSONDecodeError as e: logger.error(response.content) logger.debug(e) raise e - if response.status_code >= 400 or ("result" in response_json and response_json["result"] == "error"): - logger.debug(f"Response: {response_json}") - raise Failed(f"({response.status_code} [{response.reason}]) {response_json}") - if not response_json["id"]: - raise Failed("Gotify Error: Invalid apikey") - return response + if response.status_code >= 400: + raise Failed(f"({response.status_code} [{response.reason}]) {response_json['errorDescription']}") + return response_json + + def notification(self, json): + message = "" + if json["event"] == "run_end": + title = "Run Completed" + message = f"Start Time: {json['start_time']}\n" \ + f"End Time: {json['end_time']}\n" \ + f"Run Time: {json['run_time']}\n" \ + f"Collections Created: {json['collections_created']}\n" \ + f"Collections Modified: {json['collections_modified']}\n" \ + f"Collections Deleted: {json['collections_deleted']}\n" \ + f"Items Added: {json['items_added']}\n" \ + f"Items Removed: {json['items_removed']}" + if json["added_to_radarr"]: + message += f"\n{json['added_to_radarr']} Movies Added To Radarr" + if json["added_to_sonarr"]: + message += f"\n{json['added_to_sonarr']} Movies Added To Sonarr" + elif json["event"] == "run_start": + title = "Run Started" + message = json["start_time"] + elif json["event"] == "version": + title = "New Version Available" + message = f"Current: {json['current']}\n" \ + f"Latest: {json['latest']}\n" \ + f"Notes: {json['notes']}" + elif json["event"] == "delete": + if "library_name" in json: + title = "Collection Deleted" + else: + title = "Playlist Deleted" + message = json["message"] + else: + new_line = "\n" + if "server_name" in json: + message += f"{new_line if message else ''}Server: {json['server_name']}" + if "library_name" in json: + message += f"{new_line if message else ''}Library: {json['library_name']}" + if "collection" in json: + message += f"{new_line if message else ''}Collection: {json['collection']}" + if "playlist" in json: + message += f"{new_line if message else ''}Playlist: {json['playlist']}" + if json["event"] == "error": + if "collection" in json: + title_name = "Collection" + elif "playlist" in json: + title_name = "Playlist" + elif "library_name" in json: + title_name = "Library" + else: + title_name = "Global" + title = f"{'Critical ' if json['critical'] else ''}{title_name} Error" + message += f"{new_line if message else ''}Error Message: {json['error']}" + else: + title = f"{'Collection' if 'collection' in json else 'Playlist'} {'Created' if json['created'] else 'Modified'}" + if json['radarr_adds']: + message += f"{new_line if message else ''}{len(json['radarr_adds'])} Radarr Additions:" + if json['sonarr_adds']: + message += f"{new_line if message else ''}{len(json['sonarr_adds'])} Sonarr Additions:" + message += f"{new_line if message else ''}{len(json['additions'])} Additions:" + for add_dict in json['additions']: + message += f"\n{add_dict['title']}" + message += f"{new_line if message else ''}{len(json['removals'])} Removals:" + for add_dict in json['removals']: + message += f"\n{add_dict['title']}" + + self._request(json={"message": message, "title": title}) diff --git a/modules/library.py b/modules/library.py index 12500443..30da97fa 100644 --- a/modules/library.py +++ b/modules/library.py @@ -16,8 +16,6 @@ class Library(ABC): self.Webhooks = None self.Operations = Operations(config, self) self.Overlays = None - self.Notifiarr = None - self.Gotify = None self.collections = [] self.collection_names = [] self.metadatas = [] diff --git a/modules/webhooks.py b/modules/webhooks.py index 321d5ff0..d304f79c 100644 --- a/modules/webhooks.py +++ b/modules/webhooks.py @@ -5,7 +5,7 @@ from modules.util import Failed, YAML logger = util.logger class Webhooks: - def __init__(self, config, system_webhooks, library=None, notifiarr=None): + def __init__(self, config, system_webhooks, library=None, notifiarr=None, gotify=None): self.config = config self.error_webhooks = system_webhooks["error"] if "error" in system_webhooks else [] self.version_webhooks = system_webhooks["version"] if "version" in system_webhooks else [] @@ -14,6 +14,7 @@ class Webhooks: self.delete_webhooks = system_webhooks["delete"] if "delete" in system_webhooks else [] self.library = library self.notifiarr = notifiarr + self.gotify = gotify def _request(self, webhooks, json): logger.trace("") @@ -24,16 +25,15 @@ class Webhooks: for webhook in list(set(webhooks)): response = None logger.trace(f"Webhook: {webhook}") - if webhook == "notifiarr" or webhook == "gotify": + if webhook == "notifiarr": if self.notifiarr: for x in range(6): - if webhook == "gotify": - json = self.gotify(json) - response = self.notifiarr.notification(json) - else: - response = self.notifiarr.notification(json) + response = self.notifiarr.notification(json) if response.status_code < 500: break + elif webhook == "gotify": + if self.gotify: + self.gotify.notification(json) else: if webhook.startswith("https://discord.com/api/webhooks"): json = self.discord(json) @@ -44,17 +44,16 @@ class Webhooks: try: response_json = response.json() logger.trace(f"Response: {response_json}") - if (webhook == "notifiarr" or webhook == "gotify") and self.notifiarr and response.status_code == 400: + if webhook == "notifiarr" and self.notifiarr and response.status_code == 400: def remove_from_config(text, hook_cat): if response_json["details"]["response"] == text: yaml = YAML(self.config.config_path) changed = False if hook_cat in yaml.data and yaml.data["webhooks"][hook_cat]: - if isinstance(yaml.data["webhooks"][hook_cat], list) and ("notifiarr" in yaml.data["webhooks"][hook_cat] or "gotify" in yaml.data["webhooks"][hook_cat]): + if isinstance(yaml.data["webhooks"][hook_cat], list) and "notifiarr" in yaml.data["webhooks"][hook_cat]: changed = True yaml.data["webhooks"][hook_cat].pop("notifiarr") - yaml.data["webhooks"][hook_cat].pop("gotify") - elif yaml.data["webhooks"][hook_cat] == "notifiarr" or yaml.data["webhooks"][hook_cat] == "gotify": + elif yaml.data["webhooks"][hook_cat] == "notifiarr": changed = True yaml.data["webhooks"][hook_cat] = None if changed: @@ -67,7 +66,7 @@ class Webhooks: remove_from_config("PMM start/complete trigger is not enabled", "run_end") remove_from_config("PMM app updates trigger is not enabled", "version") if "result" in response_json and response_json["result"] == "error" and "details" in response_json and "response" in response_json["details"]: - raise Failed(f"Notifiarr/Gotify Error: {response_json['details']['response']}") + raise Failed(f"Notifiarr Error: {response_json['details']['response']}") if response.status_code >= 400 or ("result" in response_json and response_json["result"] == "error"): raise Failed(f"({response.status_code} [{response.reason}]) {response_json}") except JSONDecodeError: @@ -332,65 +331,3 @@ class Webhooks: new_json["embeds"][0]["fields"] = fields return new_json - - def gotify(self, json: dict): - message = "" - if json.get("event") == "run_end": - title = "Run Completed" - message = f"Start Time: {json['start_time']}\nEnd Time: {json['end_time']}\nRun Time: {json['run_time']}\nCollections Created: {json['collections_created']}\nCollections Modified: {json['collections_modified']}\nCollections Deleted: {json['collections_deleted']}" - if json.get("added_to_radarr"): - message = message + (f"{json['added_to_radarr']} Movies Added To Radarr\n", None) - if json.get("added_to_sonarr"): - message = message + (f"{json['added_to_sonarr']} Series Added To Sonarr\n", None) - elif json.get("event") == "run_start": - title = "Run Started" - message = json["start_time"] - elif json.get("event") == "version": - title = "New Version Available" - message = f"Current : {json['current']}\nLatest: {json['latest']}\nNew Commits: {json['notes']}" - else: - message1 = "" - text = "" - if "server_name" in json: - message1 = message1 + f"Server: {json['server_name']}\n" - if "library_name" in json: - message1 = message1 + f"Library: {json['library_name']}\n" - if "collection" in json: - text = "Collection" - message1 = message1 + f"Collection: {json['collection']}\n" - elif "playlist" in json: - text = "Playlist" - message1 = message1 + f"Playlist: {json['playlist']}\n" - if message1: - message1 = message1 + "\n" - if json["event"] == "delete": - title = json["message"] - elif "error" in json: - title = f"{'Critical ' if json['critical'] else ''}Error" - message = message + f"Error Message: {json['error']}\n" - else: - title = f"{text} {'Created' if json['created'] else 'Modified'}" - - def get_field_text(items_list): - field_text = "" - for i, item in enumerate(items_list, 1): - field_text += f"\n{i}. {item['title']}" - return field_text - - if json["additions"]: - message = message + f"Items Added: { get_field_text(json['additions'])}\n" - if json["removals"]: - message = message + f"Items Removed: { get_field_text(json['removals'])}\n" - - gotify_json = { - "message": "", - "priority": 1, - "title": "" - } - if message: - gotify_json["message"] = message - - if title: - gotify_json["title"] = title - - return gotify_json diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 8bfb817b..a2c14599 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -927,11 +927,11 @@ def run_playlists(config): #logger.add_playlist_handler(playlist_log_name) status[mapping_name] = {"status": "Unchanged", "errors": [], "added": 0, "unchanged": 0, "removed": 0, "radarr": 0, "sonarr": 0} server_name = None - library_names = None try: builder = CollectionBuilder(config, playlist_file, mapping_name, playlist_attrs, extra=output_str) stats["names"].append(builder.name) logger.info("") + server_name = builder.libraries[0].PlexServer.friendlyName logger.separator(f"Running {mapping_name} Playlist", space=False, border=False) @@ -1049,7 +1049,7 @@ def run_playlists(config): except Deleted as e: logger.info(e) status[mapping_name]["status"] = "Deleted" - config.notify_delete(e) + config.notify_delete(e, server=server_name) except NotScheduled as e: logger.info(e) if str(e).endswith("and was deleted"): @@ -1059,13 +1059,13 @@ def run_playlists(config): else: status[mapping_name]["status"] = "Not Scheduled" except Failed as e: - config.notify(e, server=server_name, library=library_names, playlist=mapping_name) + config.notify(e, server=server_name, playlist=mapping_name) logger.stacktrace() logger.error(e) status[mapping_name]["status"] = "PMM Failure" status[mapping_name]["errors"].append(e) except Exception as e: - config.notify(f"Unknown Error: {e}", server=server_name, library=library_names, playlist=mapping_name) + config.notify(f"Unknown Error: {e}", server=server_name, playlist=mapping_name) logger.stacktrace() logger.error(f"Unknown Error: {e}") status[mapping_name]["status"] = "Unknown Error"