[21] Fix Gotify PR

This commit is contained in:
meisnate12 2024-01-26 12:16:10 -05:00
parent fe9d0749bd
commit 33b92f2a2d
12 changed files with 116 additions and 142 deletions

View file

@ -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

View file

@ -1 +1 @@
1.20.0-develop20
1.20.0-develop21

View file

@ -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: ######

View file

@ -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 } |
| `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.

View file

@ -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
}
```

View file

@ -293,13 +293,13 @@
"url": {
"type": "string"
},
"apikey": {
"token": {
"type": "string"
}
},
"required": [
"url",
"apikey"
"token"
],
"title": "gotify"

View file

@ -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

View file

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

View file

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

View file

@ -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 = []

View file

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

View file

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