Skip to content

Commit

Permalink
Add ntfy notification agent (#2475)
Browse files Browse the repository at this point in the history
  • Loading branch information
nwithan8 authored Jan 24, 2025
1 parent d26e02d commit 208785b
Show file tree
Hide file tree
Showing 14 changed files with 246 additions and 70 deletions.
1 change: 1 addition & 0 deletions .github/.wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ nl
nokmål
nosuchlibrary
notifiarr
ntfy
num
nynorsk
oauth
Expand Down
3 changes: 2 additions & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Added `plex` ratings source for mass operations.
Added IMDb Interests (sub-genres) to `imdb_search` builder
Allow `server_preroll` to accept a list
Changed default `overlay_artwork_filetype` to `webp_lossy` and `overlay_artwork_quality` to 90
Added `ntfy` as a notification option

# Docs
Added "getting started" page
Expand Down Expand Up @@ -74,4 +75,4 @@ Fixes #2385 `tmdb_person` would pass an integer if the name started with an inte
Fixes an issue where `show_missing` would display missing movies against show libraries (closes #2351)
Fixed an OMDb API issue where API key would intermittently be treated as invalid
Fixed an issue where Kometa would try to upload and cache images larger than Plex allows (10mb is the upper limit)
Fixes an issue where `use_subtitles` would ignore `flag_alignment: left`
Fixes an issue where `use_subtitles` would ignore `flag_alignment: left`
4 changes: 4 additions & 0 deletions config/config.yml.template
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ notifiarr:
gotify:
url: http://192.168.1.12:80
token: ####################################
ntfy:
url: http://192.168.1.12:80
token: ####################################
topic: kometa
anidb: # Not required for AniDB builders unless you want mature content
username: ######
password: ######
Expand Down
4 changes: 2 additions & 2 deletions docs/config/gotify.md
Original file line number Diff line number Diff line change
@@ -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.

Expand All @@ -18,7 +18,7 @@ gotify:
| `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
Once you have added the configuration data your config.yml you have to add `gotify` to any [webhook](webhooks.md) to send that
notification to Gotify.

```yaml
Expand Down
58 changes: 58 additions & 0 deletions docs/config/ntfy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# ntfy Attributes

Configuring [ntfy](https://ntfy.sh/) is optional but can allow you to send the [webhooks](webhooks.md) straight to ntfy.

## Setup

Users can either use the [public ntfy server](https://ntfy.sh), or [host their own ntfy server](https://docs.ntfy.sh/install/).

### Retrieving Access Token and Topic

#### Using the Public Server

1. Visit https://ntfy.sh/login and login or sign up for an account (it's free).
2. Select "[Account](https://ntfy.sh/account)" from the side menu and scroll to "Access Tokens" to generate an access token.
3. Copy the access token and paste it into the `token` attribute in your config file. Enter `https://ntfy.sh` into the `url` attribute.
4. Click "Subscribe to topic" from the side menu and enter a topic name.
- Pro subscribers can reserve specific topics, but any free tier user can subscribe and publish to any non-reserved topic.
- Common topics such as `kometa` are likely already reserved or at least used by other public ntfy users (and these topics may be visible to the general public). It's recommended to use a random topic name to keep your notifications semi-private.
5. Enter the topic name into the `topic` attribute in the config file.


#### Using a Self-Hosted Server

If you are a standard (non-admin) user of a [ntfy server other than the official one](https://docs.ntfy.sh/integrations/#alternative-ntfy-servers), you can follow the same steps as above, but with the server URL and token provided by the server.

If you are an admin of your own ntfy server, you can follow these steps:

1. Follow the [installation instructions](https://docs.ntfy.sh/install/) to set up your own ntfy server.
2. Follow the same steps above for creating an account and access token, or use the `ntfy` command line tool to [create a user](https://docs.ntfy.sh/config/#users-and-roles) and [generate an access token](https://docs.ntfy.sh/config/#access-tokens).
3. Follow the same steps as above for generating/reserving a topic, but with the server URL and token provided by your server.

### Configuring `ntfy` in the Config File

Use the URL, topic and token to configure `ntfy` in the root of the config file:

```yaml
ntfy:
url: https://ntfy.sh # or a different ntfy server URL
token: tk_thisismyaccesstoken
topic: kometa # or a different topic name
```
| Attribute | Allowed Values | Required |
|:----------|:-----------------------|:------------------------------------------:|
| `url` | ntfy Server Url | :fontawesome-solid-circle-check:{ .green } |
| `token` | ntfy User Access Token | :fontawesome-solid-circle-check:{ .green } |
| `topic` | ntfy Topic | :fontawesome-solid-circle-check:{ .green } |

Once you have added the configuration data to your `config.yml`, you can add `ntfy` to any [webhook](webhooks.md) to send that notification to ntfy.

```yaml
webhooks:
error: ntfy
version: ntfy
run_start: ntfy
run_end: ntfy
changes: ntfy
```
1 change: 1 addition & 0 deletions docs/config/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ requirements for setup that can be found by clicking the links within the table
| [`mdblist`](mdblist.md) | :fontawesome-solid-circle-xmark:{ .red } |
| [`notifiarr`](notifiarr.md) | :fontawesome-solid-circle-xmark:{ .red } |
| [`gotify`](gotify.md) | :fontawesome-solid-circle-xmark:{ .red } |
| [`ntfy`](ntfy.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 } |
Expand Down
2 changes: 2 additions & 0 deletions docs/config/webhooks.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ webhooks:
run_start:
- notifiarr
- gotify
- ntfy
run_end:
- https://www.myspecialdomain.com/kometa
- https://www.myotherdomain.com/kometa
Expand All @@ -42,6 +43,7 @@ webhooks:

* 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.
* To send notifications to [ntfy](ntfy.md) just add `ntfy` to a webhook instead of the webhook url.

## Error Notifications

Expand Down
26 changes: 25 additions & 1 deletion json-schema/config-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@
"gotify": {
"$ref": "#/definitions/gotify-api"
},
"ntfy": {
"$ref": "#/definitions/ntfy-api"
},
"anidb": {
"$ref": "#/definitions/anidb-api"
},
Expand Down Expand Up @@ -307,6 +310,27 @@
],
"title": "gotify"
},
"ntfy-api": {
"type": "object",
"additionalProperties": false,
"properties": {
"url": {
"type": "string"
},
"token": {
"type": "string"
},
"topic": {
"type": "string"
}
},
"required": [
"url",
"token",
"topic"
],
"title": "ntfy"
},
"anidb-api": {
"type": "object",
"additionalProperties": false,
Expand Down Expand Up @@ -1532,7 +1556,7 @@
"type": "object",
"additionalProperties": false,
"patternProperties": {
"^(?!plex|tmdb|tautulli|webhooks|omdb|mdblist|notifiarr|gotify|anidb|radarr|sonarr|trakt|mal).+$": {
"^(?!plex|tmdb|tautulli|webhooks|omdb|mdblist|notifiarr|gotify|ntfy|anidb|radarr|sonarr|trakt|mal).+$": {
"additionalProperties": false,
"properties": {
"metadata_files": {
Expand Down
4 changes: 4 additions & 0 deletions json-schema/prototype_config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,10 @@ notifiarr:
gotify:
url: http://192.168.1.12:80
token: Enter Gotify token
ntfy:
url: http://192.168.1.12:80
token: Enter ntfy access token
topic: Enter ntfy topic
anidb: # Not required for AniDB builders unless you want mature content
username: Enter AniDB Username
password: Enter AniDB Password
Expand Down
1 change: 1 addition & 0 deletions mkdocs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ nav:
- MDBList: config/mdblist.md
- Notifiarr: config/notifiarr.md
- Gotify: config/gotify.md
- ntfy: config/ntfy.md
- AniDB: config/anidb.md
- Radarr: config/radarr.md
- Sonarr: config/sonarr.md
Expand Down
23 changes: 22 additions & 1 deletion modules/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from modules.mojo import BoxOfficeMojo
from modules.notifiarr import Notifiarr
from modules.gotify import Gotify
from modules.ntfy import Ntfy
from modules.omdb import OMDb
from modules.overlays import Overlays
from modules.plex import Plex
Expand Down Expand Up @@ -339,6 +340,7 @@ def hooks(hook_attr):
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 "ntfy" in self.data: self.data["ntfy"] = self.data.pop("ntfy")
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):
Expand Down Expand Up @@ -598,6 +600,25 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, translatio
else:
logger.info("gotify attribute not found")

self.NtfyFactory = None
if "ntfy" in self.data:
logger.info("Connecting to ntfy...")
try:
self.NtfyFactory = Ntfy(self.Requests, {
"url": check_for_attribute(self.data, "url", parent="ntfy", throw=True),
"token": check_for_attribute(self.data, "token", parent="ntfy", throw=True),
"topic": check_for_attribute(self.data, "topic", parent="ntfy", throw=True)
})
except Failed as e:
if str(e).endswith("is blank"):
logger.warning(e)
else:
logger.stacktrace()
logger.error(e)
logger.info(f"ntfy Connection {'Failed' if self.NtfyFactory is None else 'Successful'}")
else:
logger.info("ntfy 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),
Expand All @@ -606,7 +627,7 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, translatio
"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, gotify=self.GotifyFactory)
self.Webhooks = Webhooks(self, self.webhooks, notifiarr=self.NotifiarrFactory, gotify=self.GotifyFactory, ntfy=self.NtfyFactory)
try:
self.Webhooks.start_time_hooks(self.start_time)
if self.Requests.has_new_version():
Expand Down
67 changes: 3 additions & 64 deletions modules/gotify.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
from json import JSONDecodeError
from modules import util
from modules import util, webhooks
from modules.util import Failed

logger = util.logger


class Gotify:
def __init__(self, requests, params):
self.requests = requests
Expand Down Expand Up @@ -33,67 +34,5 @@ def _request(self, path="message", json=None, post=True):
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']}"

message, title, _ = webhooks.get_message(json)
self._request(json={"message": message, "title": title})
44 changes: 44 additions & 0 deletions modules/ntfy.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
from modules import util, webhooks
from modules.util import Failed

logger = util.logger


class Ntfy:
def __init__(self, requests, params):
self.requests = requests
self.token = params["token"]
self.url = params["url"].rstrip("/")
self.topic = params["topic"]

logger.secret(self.url)
logger.secret(self.token)

self._test_url()

def _test_url(self):
try:
self._request(message="Kometa - Testing ntfy Access", priority=1)
except Exception:
logger.stacktrace()
raise Failed("ntfy Error: Invalid details")

def _request(self, message: str, title: str = None, priority: int = 3):
headers = {
"Authorization": f"Bearer {self.token}",
"Icon": "https://kometa.wiki/en/latest/assets/icon.png",
"Priority": str(priority)
}
if title:
headers["Title"] = title

response = self.requests.post(f"{self.url}/{self.topic}", headers=headers, data=message)

if not response:
raise Failed(f"({response.status_code} [{response.reason}]) {response.content}")

return response

def notification(self, json):
message, title, priority = webhooks.get_message(json)
self._request(message=message, title=title, priority=priority)
Loading

0 comments on commit 208785b

Please sign in to comment.