From 459710d8e61bc03d55b8c639f095f9970b054430 Mon Sep 17 00:00:00 2001 From: krstn Date: Thu, 18 Jan 2024 01:23:06 +0100 Subject: [PATCH 1/3] gotify api --- config/config.yml.template | 3 + docs/config/gotify.md | 31 +++++++++++ docs/config/overview.md | 1 + docs/config/webhooks.md | 2 +- json-schema/config-schema.json | 23 +++++++- json-schema/prototype_config.yml | 3 + mkdocs.yml | 1 + modules/config.py | 40 +++++++++++++- modules/gotify.py | 42 ++++++++++++++ modules/library.py | 1 + modules/webhooks.py | 95 +++++++++++++++++++++++++++++--- 11 files changed, 232 insertions(+), 10 deletions(-) create mode 100644 docs/config/gotify.md create mode 100644 modules/gotify.py diff --git a/config/config.yml.template b/config/config.yml.template index 7735c9c3..606bb747 100644 --- a/config/config.yml.template +++ b/config/config.yml.template @@ -105,6 +105,9 @@ mdblist: cache_expiration: 60 notifiarr: apikey: #################################### +gotify: + url: http://192.168.1.12:80 + apikey: #################################### 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 new file mode 100644 index 00000000..43814c25 --- /dev/null +++ b/docs/config/gotify.md @@ -0,0 +1,31 @@ +# Gotify Attributes + +Configuring [Gotify](https://gotify.net/) is optional but can allow you to send the [webhooks](webhooks.md) +straight to gotify.. + +A `gotify` mapping is in the root of the config file. + +Below is a `gotify` mapping example and the full set of attributes: + +```yaml +gotify: + url: #################################### + apikey: #################################### +``` + +| Attribute | Allowed Values | Required | +|:----------|:-----------------------------------------|:------------------------------------------:| +| `url` | Gotify Server Url | :fontawesome-solid-circle-check:{ .green } | +| `apikey` | Gotify Application API Key | :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. + +```yaml +webhooks: + error: gotify + version: gotify + run_start: gotify + run_end: gotify + changes: gotify +``` diff --git a/docs/config/overview.md b/docs/config/overview.md index 5e939f5f..2f23ae8b 100644 --- a/docs/config/overview.md +++ b/docs/config/overview.md @@ -24,6 +24,7 @@ requirements for setup that can be found by clicking the links within the table. | [`tautulli`](tautulli.md) | :fontawesome-solid-circle-xmark:{ .red } | | [`omdb`](omdb.md) | :fontawesome-solid-circle-xmark:{ .red } | | [`notifiarr`](notifiarr.md) | :fontawesome-solid-circle-xmark:{ .red } | +| [`gotify`](gotify.md) | :fontawesome-solid-circle-xmark:{ .red } | | [`anidb`](anidb.md) | :fontawesome-solid-circle-xmark:{ .red } | | [`radarr`](radarr.md) | :fontawesome-solid-circle-xmark:{ .red } | | [`sonarr`](sonarr.md) | :fontawesome-solid-circle-xmark:{ .red } | diff --git a/docs/config/webhooks.md b/docs/config/webhooks.md index d760ec8d..6f58ea07 100644 --- a/docs/config/webhooks.md +++ b/docs/config/webhooks.md @@ -27,7 +27,7 @@ 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) just add `notifiarr` to a webhook instead of the webhook url. +* To send notifications to [Notifiarr](notifiarr.md) or [Gotify](gotify.md) just add `notifiarr` or `gotify` to a webhook instead of the webhook url. ## Error Notifications diff --git a/json-schema/config-schema.json b/json-schema/config-schema.json index ee53221e..5912a5c9 100644 --- a/json-schema/config-schema.json +++ b/json-schema/config-schema.json @@ -25,6 +25,9 @@ "notifiarr": { "$ref": "#/definitions/notifiarr-api" }, + "gotify": { + "$ref": "#/definitions/gotify-api" + }, "anidb": { "$ref": "#/definitions/anidb-api" }, @@ -253,6 +256,24 @@ ], "title": "notifiarr" }, + "gotify-api": { + "type": "object", + "additionalProperties": false, + "properties": { + "url": { + "type": "string" + }, + "apikey": { + "type": "string" + } + }, + "required": [ + "url", + "apikey" + + ], + "title": "gotify" + }, "anidb-api": { "type": "object", "additionalProperties": false, @@ -902,7 +923,7 @@ "type": "object", "additionalProperties": false, "patternProperties": { - "^(?!plex|tmdb|tautulli|webhooks|omdb|mdblist|notifiarr|anidb|radarr|sonarr|trakt|mal).+$": { + "^(?!plex|tmdb|tautulli|webhooks|omdb|mdblist|notifiarr|gotify|anidb|radarr|sonarr|trakt|mal).+$": { "additionalProperties": false, "properties": { "metadata_files": { diff --git a/json-schema/prototype_config.yml b/json-schema/prototype_config.yml index 3af505c4..60d3ec64 100644 --- a/json-schema/prototype_config.yml +++ b/json-schema/prototype_config.yml @@ -472,6 +472,9 @@ mdblist: cache_expiration: 60 notifiarr: apikey: this-is-a-placeholder-string +gotify: + url: http://192.168.1.12:80 + apikey: 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/mkdocs.yml b/mkdocs.yml index b4d4fbb4..90de5247 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -177,6 +177,7 @@ nav: - Radarr: config/radarr.md - Sonarr: config/sonarr.md - Notifiarr: config/notifiarr.md + - Gotify: config/gotify.md - Tautulli: config/tautulli.md - Github: config/github.md - MdbList: config/mdblist.md diff --git a/modules/config.py b/modules/config.py index 23cc69c4..0c61e71e 100644 --- a/modules/config.py +++ b/modules/config.py @@ -15,6 +15,7 @@ from modules.letterboxd import Letterboxd from modules.mal import MyAnimeList from modules.meta import PlaylistFile from modules.notifiarr import Notifiarr +from modules.gotify import Gotify from modules.omdb import OMDb from modules.overlays import Overlays from modules.plex import Plex @@ -286,6 +287,7 @@ class ConfigFile: if "omdb" in self.data: self.data["omdb"] = self.data.pop("omdb") if "mdblist" in self.data: self.data["mdblist"] = self.data.pop("mdblist") if "notifiarr" in self.data: self.data["notifiarr"] = self.data.pop("notifiarr") + if "gotify" in self.data: self.data["gotify"] = self.data.pop("gotify") if "anidb" in self.data: self.data["anidb"] = self.data.pop("anidb") if "radarr" in self.data: if "monitor" in self.data["radarr"] and isinstance(self.data["radarr"]["monitor"], bool): @@ -543,6 +545,42 @@ class ConfigFile: 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)}) + except Failed as e: + if str(e).endswith("is blank"): + logger.warning(e) + else: + logger.stacktrace() + logger.error(e) + logger.info(f"Gotify Connection {'Failed' if self.GotifyFactory is None else 'Successful'}") + else: + logger.info("gotify 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.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]): + 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() + try: self.TMDb = None if "tmdb" in self.data: @@ -1173,7 +1211,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) + library.Webhooks = Webhooks(self, {}, library=library, notifiarr=self.NotifiarrFactory or self.GotifyFactory,) library.Overlays = Overlays(self, library) logger.info("") diff --git a/modules/gotify.py b/modules/gotify.py new file mode 100644 index 00000000..0bca93fb --- /dev/null +++ b/modules/gotify.py @@ -0,0 +1,42 @@ +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) + try: + self.request(path="message") + except JSONDecodeError: + raise Failed("Gotify Error: Invalid JSON response received") + + 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) + 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 diff --git a/modules/library.py b/modules/library.py index 1cf93597..a730698c 100644 --- a/modules/library.py +++ b/modules/library.py @@ -17,6 +17,7 @@ class Library(ABC): 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 c7d776cf..d52da78a 100644 --- a/modules/webhooks.py +++ b/modules/webhooks.py @@ -1,4 +1,4 @@ -from json import JSONDecodeError +from json import JSONDecodeError, dumps from modules import util from modules.util import Failed, YAML @@ -24,10 +24,14 @@ class Webhooks: for webhook in list(set(webhooks)): response = None logger.trace(f"Webhook: {webhook}") - if webhook == "notifiarr": + if webhook == "notifiarr" or webhook == "gotify": if self.notifiarr: for x in range(6): - response = self.notifiarr.notification(json) + if webhook == "gotify": + json = self.gotify(json) + response = self.notifiarr.notification(json) + else: + response = self.notifiarr.notification(json) if response.status_code < 500: break else: @@ -40,16 +44,17 @@ class Webhooks: try: response_json = response.json() logger.trace(f"Response: {response_json}") - if webhook == "notifiarr" and self.notifiarr and response.status_code == 400: + if (webhook == "notifiarr" or webhook == "gotify") 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]: + if isinstance(yaml.data["webhooks"][hook_cat], list) and ("notifiarr" in yaml.data["webhooks"][hook_cat] or "gotify" in yaml.data["webhooks"][hook_cat]): changed = True yaml.data["webhooks"][hook_cat].pop("notifiarr") - elif yaml.data["webhooks"][hook_cat] == "notifiarr": + yaml.data["webhooks"][hook_cat].pop("gotify") + elif yaml.data["webhooks"][hook_cat] == "notifiarr" or yaml.data["webhooks"][hook_cat] == "gotify": changed = True yaml.data["webhooks"][hook_cat] = None if changed: @@ -62,7 +67,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 Error: {response_json['details']['response']}") + raise Failed(f"Notifiarr/Gotify 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: @@ -326,3 +331,79 @@ class Webhooks: fields.append(field) 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 = [ + [("Start Time", json["start_time"]), ("End Time", json["end_time"]), ("Run Time", json["run_time"])], + [("Collections", None)], + [ + ("Created", json["collections_created"] if json["collections_created"] else "0"), + ("Modified", json["collections_modified"] if json["collections_modified"] else "0"), + ("Deleted", json["collections_deleted"] if json["collections_deleted"] else "0") + ] + ] + 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 = [ + [("Current", json["current"]), ("Latest", json["latest"])], + [("New Commits", json["notes"])] + ] + 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 From b7e1c6672a3db366341f3ec93ddee15411d610e7 Mon Sep 17 00:00:00 2001 From: krstn Date: Thu, 18 Jan 2024 01:39:27 +0100 Subject: [PATCH 2/3] cleanup --- modules/webhooks.py | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/modules/webhooks.py b/modules/webhooks.py index d52da78a..321d5ff0 100644 --- a/modules/webhooks.py +++ b/modules/webhooks.py @@ -1,4 +1,4 @@ -from json import JSONDecodeError, dumps +from json import JSONDecodeError from modules import util from modules.util import Failed, YAML @@ -337,15 +337,6 @@ class Webhooks: message = "" if json.get("event") == "run_end": title = "Run Completed" - message = [ - [("Start Time", json["start_time"]), ("End Time", json["end_time"]), ("Run Time", json["run_time"])], - [("Collections", None)], - [ - ("Created", json["collections_created"] if json["collections_created"] else "0"), - ("Modified", json["collections_modified"] if json["collections_modified"] else "0"), - ("Deleted", json["collections_deleted"] if json["collections_deleted"] else "0") - ] - ] 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) @@ -356,10 +347,6 @@ class Webhooks: message = json["start_time"] elif json.get("event") == "version": title = "New Version Available" - message = [ - [("Current", json["current"]), ("Latest", json["latest"])], - [("New Commits", json["notes"])] - ] message = f"Current : {json['current']}\nLatest: {json['latest']}\nNew Commits: {json['notes']}" else: message1 = "" From 3d7bda8b5d64779a549723d1e6aacde84adbba5d Mon Sep 17 00:00:00 2001 From: krstn Date: Thu, 18 Jan 2024 13:15:24 +0100 Subject: [PATCH 3/3] removing dot --- docs/config/gotify.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/config/gotify.md b/docs/config/gotify.md index 43814c25..0eac99dd 100644 --- a/docs/config/gotify.md +++ b/docs/config/gotify.md @@ -1,7 +1,7 @@ # Gotify Attributes Configuring [Gotify](https://gotify.net/) is optional but can allow you to send the [webhooks](webhooks.md) -straight to gotify.. +straight to gotify. A `gotify` mapping is in the root of the config file.