diff --git a/config/config.yml.template b/config/config.yml.template index 79a00919..5b43a781 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..0eac99dd --- /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 cf239755..a681f84f 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" }, @@ -283,6 +286,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, @@ -1116,7 +1137,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 12af256d..2b4f0c11 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -178,6 +178,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 882557d1..22066607 100644 --- a/modules/config.py +++ b/modules/config.py @@ -16,6 +16,7 @@ from modules.mal import MyAnimeList from modules.meta import PlaylistFile from modules.mojo import BoxOfficeMojo 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 @@ -289,6 +290,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): @@ -546,6 +548,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: @@ -1205,7 +1243,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 7f1e3f8a..12500443 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..321d5ff0 100644 --- a/modules/webhooks.py +++ b/modules/webhooks.py @@ -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,66 @@ 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 = 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