diff --git a/app/library/DownloadQueue.py b/app/library/DownloadQueue.py index 67148ea3..fd169a10 100644 --- a/app/library/DownloadQueue.py +++ b/app/library/DownloadQueue.py @@ -494,7 +494,12 @@ class DownloadQueue(metaclass=Singleton): dlInfo.info.status = "not_live" dlInfo.info.msg = nMessage.replace(f" '{dlInfo.info.title}'", "") - await self._notify.emit(Events.LOG_INFO, data={"lowPriority": True}, title=nTitle, message=nMessage) + await self._notify.emit( + Events.LOG_INFO, + data={"preset": dlInfo.info.preset, "lowPriority": True}, + title=nTitle, + message=nMessage, + ) itemDownload: Download = self.done.put(dlInfo) elif len(entry.get("formats", [])) < 1: @@ -511,7 +516,12 @@ class DownloadQueue(metaclass=Singleton): dlInfo.info.status = "error" itemDownload = self.done.put(dlInfo) - await self._notify.emit(Events.LOG_WARNING, data={"logs": text_logs}, title=nTitle, message=nMessage) + await self._notify.emit( + Events.LOG_WARNING, + data={"preset": dlInfo.info.preset, "logs": text_logs}, + title=nTitle, + message=nMessage, + ) elif is_premiere and self.config.prevent_live_premiere: nStore = "history" nTitle = "Premiere Video" @@ -546,7 +556,9 @@ class DownloadQueue(metaclass=Singleton): nStore = "history" nEvent = Events.ITEM_MOVED nTitle = "Premiering right now" - await self._notify.emit(Events.LOG_INFO, title=nTitle, message=nMessage) + await self._notify.emit( + Events.LOG_INFO, data={"preset": dlInfo.info.preset}, title=nTitle, message=nMessage + ) else: nEvent = Events.ITEM_ADDED nTitle = "Item Added" @@ -559,7 +571,9 @@ class DownloadQueue(metaclass=Singleton): await self._notify.emit( nEvent, - data={"to": nStore, "item": itemDownload.info} if Events.ITEM_MOVED == nEvent else itemDownload.info, + data={"to": nStore, "preset": itemDownload.info.preset, "item": itemDownload.info} + if Events.ITEM_MOVED == nEvent + else itemDownload.info, title=nTitle, message=nMessage, ) @@ -664,7 +678,9 @@ class DownloadQueue(metaclass=Singleton): if item.is_archived(): message: str = f"The URL '{item.url}' is already downloaded and recorded in archive." LOG.error(message) - await self._notify.emit(Events.LOG_INFO, title="Already Downloaded", message=message) + await self._notify.emit( + Events.LOG_INFO, data={"preset": item.preset}, title="Already Downloaded", message=message + ) return {"status": "error", "msg": message} started: float = time.perf_counter() @@ -777,7 +793,7 @@ class DownloadQueue(metaclass=Singleton): self.done.put(item) await self._notify.emit( Events.ITEM_MOVED, - data={"to": "history", "item": item.info}, + data={"to": "history", "preset": item.info.preset, "item": item.info}, title="Download Cancelled", message=f"Download '{item.info.title}' has been cancelled.", ) @@ -1014,7 +1030,7 @@ class DownloadQueue(metaclass=Singleton): _tasks.append( self._notify.emit( Events.ITEM_MOVED, - data={"to": "history", "item": entry.info}, + data={"to": "history", "preset": entry.info.preset, "item": entry.info}, title=nTitle, message=nMessage, ) diff --git a/app/library/Notifications.py b/app/library/Notifications.py index c90a0b33..4020f955 100644 --- a/app/library/Notifications.py +++ b/app/library/Notifications.py @@ -15,7 +15,8 @@ from .BackgroundWorker import BackgroundWorker from .config import Config from .encoder import Encoder from .Events import Event, EventBus, Events -from .ItemDTO import ItemDTO +from .ItemDTO import Item, ItemDTO +from .Presets import Preset, Presets from .Singleton import Singleton from .Utils import validate_uuid @@ -71,7 +72,8 @@ class Target: id: str name: str - on: list[str] + on: list[str] = field(default_factory=list) + presets: list[str] = field(default_factory=list) request: TargetRequest def serialize(self) -> dict: @@ -79,6 +81,7 @@ class Target: "id": self.id, "name": self.name, "on": self.on, + "presets": self.presets, "request": self.request.serialize(), } @@ -144,11 +147,11 @@ class Notification(metaclass=Singleton): Notification._instance = self config: Config = config or Config.get_instance() - self._debug = config.debug + self._debug: bool = config.debug self._file: Path = Path(file) if file else Path(config.config_path).joinpath("notifications.json") self._client: httpx.AsyncClient = client or httpx.AsyncClient() self._encoder: Encoder = encoder or Encoder() - self._version = config.app_version + self._version: str = config.app_version self._offload: BackgroundWorker = background_worker or BackgroundWorker.get_instance() if self._file.exists() and "600" != self._file.stat().st_mode: @@ -212,7 +215,7 @@ class Notification(metaclass=Singleton): if not self._file.exists() or self._file.stat().st_size < 1: return self - targets = [] + targets: list = [] try: LOG.info(f"Loading '{self._file}'.") @@ -255,6 +258,7 @@ class Notification(metaclass=Singleton): id=target.get("id"), name=target.get("name"), on=target.get("on", []), + presets=target.get("presets", []), request=TargetRequest( type=target.get("request", {}).get("type", "json"), method=target.get("request", {}).get("method", "POST"), @@ -317,8 +321,8 @@ class Notification(metaclass=Singleton): msg = "Invalid notification target. Invalid 'on' event list found." raise ValueError(msg) - removed_events = [] - all_events = NotificationEvents.get_events().values() + removed_events: list = [] + all_events: dict[str, str] = NotificationEvents.get_events().values() for e in target["on"]: if e not in all_events: removed_events.append(e) @@ -329,6 +333,24 @@ class Notification(metaclass=Singleton): msg: str = f"Invalid notification target. Invalid events '{', '.join(removed_events)}' found." raise ValueError(msg) + if "presets" in target: + if not isinstance(target["presets"], list): + msg = "Invalid notification target. Invalid 'presets' list found." + raise ValueError(msg) + + removed_presets: list = [] + all_presets: list[Preset] = Presets.get_instance().get_all() + + for p in target["presets"]: + if p not in [ap.name for ap in all_presets]: + removed_presets.append(p) + target["presets"].remove(p) + continue + + if len(removed_presets) > 0 and len(target["presets"]) < 1: + msg: str = f"Invalid notification target. Invalid presets '{', '.join(removed_presets)}' found." + raise ValueError(msg) + if "headers" in target["request"]: if not isinstance(target["request"]["headers"], list): msg = "Invalid notification target. Invalid headers list found." @@ -360,6 +382,9 @@ class Notification(metaclass=Singleton): if len(target.on) > 0 and ev.event not in target.on and "test" != ev.event: continue + if "test" != ev.event and not self._check_preset(target, ev): + continue + if not target.request.url.startswith("http"): apprise_targets.append(target) else: @@ -373,6 +398,29 @@ class Notification(metaclass=Singleton): return tasks + def _check_preset(self, target: Target, ev: Event) -> bool: + if len(target.presets) < 1: + return True + + if not isinstance(ev.data, (Item, ItemDTO, dict)): + return False + + preset_name: str | None = None + + if isinstance(ev.data, Item): + preset_name = ev.data.preset + + if isinstance(ev.data, ItemDTO): + preset_name = ev.data.preset + + if isinstance(ev.data, dict): + preset_name = ev.data.get("preset", None) + + if not preset_name: + return False + + return preset_name in target.presets + async def _apprise(self, target: list[Target], ev: Event) -> dict: if not target or not isinstance(target, list): return {} @@ -406,7 +454,7 @@ class Notification(metaclass=Singleton): try: LOG.info(f"Sending Notification event '{ev.event}: {ev.id}' to '{target.name}'.") - reqBody = { + reqBody: dict[str, Any] = { "method": target.request.method.upper(), "url": target.request.url, "headers": { @@ -423,7 +471,7 @@ class Notification(metaclass=Singleton): for h in target.request.headers: reqBody["headers"][h.key] = h.value - body_key = "json" if "json" == target.request.type.lower() else "data" + body_key: str = "json" if "json" == target.request.type.lower() else "data" reqBody[body_key] = self._deep_unpack(ev.serialize()) if "data" != target.request.data_key: @@ -437,11 +485,15 @@ class Notification(metaclass=Singleton): response = await self._client.request(**reqBody) - respData = {"url": target.request.url, "status": response.status_code, "text": response.text} + respData: dict[str, Any] = { + "url": target.request.url, + "status": response.status_code, + "text": response.text, + } - msg = f"Notification target '{target.name}' Responded to event '{ev.event}: {ev.id}' with status '{response.status_code}'." + msg: str = f"Notification target '{target.name}' Responded to event '{ev.event}: {ev.id}' with status '{response.status_code}'." if self._debug and respData.get("text"): - msg += f" body '{respData.get('text','??')}'." + msg += f" body '{respData.get('text', '??')}'." LOG.info(msg) @@ -449,7 +501,7 @@ class Notification(metaclass=Singleton): except Exception as e: err_msg = str(e) if not err_msg: - err_msg = type(e).__name__ + err_msg: str = type(e).__name__ LOG.error(f"Error sending Notification event '{ev.event}: {ev.id}' to '{target.name}'. '{err_msg!s}'.") return {"url": target.request.url, "status": 500, "text": str(ev)} diff --git a/app/library/Tasks.py b/app/library/Tasks.py index 33f70856..7cc2af27 100644 --- a/app/library/Tasks.py +++ b/app/library/Tasks.py @@ -434,13 +434,13 @@ class Tasks(metaclass=Singleton): _tasks = [ self._notify.emit( Events.TASK_DISPATCHED, - data=status, + data={**status, "preset": task.preset} if status else {"preset": task.preset}, title=f"Task '{task.name}' dispatched", message=f"Task '{task.name}' dispatched at '{timeNow}'.", ), self._notify.emit( Events.LOG_SUCCESS, - data={"lowPriority": True}, + data={"preset": task.preset, "lowPriority": True}, title="Task completed", message=f"Task '{task.name}' completed in '{ended - started:.2f}'.", ), @@ -451,6 +451,7 @@ class Tasks(metaclass=Singleton): LOG.error(f"Failed to execute '{task.name}' at '{timeNow}'. '{e!s}'.") await self._notify.emit( Events.LOG_ERROR, + data={"preset": task.preset}, title="Task failed", message=f"Failed to execute '{task.name}'. '{e!s}'", ) diff --git a/app/routes/socket/history.py b/app/routes/socket/history.py index 39421469..d86e5d39 100644 --- a/app/routes/socket/history.py +++ b/app/routes/socket/history.py @@ -15,18 +15,21 @@ async def add_url(queue: DownloadQueue, notify: EventBus, sid: str, data: dict): await notify.emit(Events.LOG_ERROR, title="Invalid request", message="No URL provided.", to=sid) return + item: Item = Item.format(data) try: - status = await queue.add(item=Item.format(data)) + status = await queue.add(item=item) await notify.emit( event=Events.ITEM_STATUS, title="Adding URL", message=f"Adding URL '{url}' to the download queue.", - data=status, + data={**status, "preset": item.preset} if status else {"preset": item.preset}, to=sid, ) except ValueError as e: LOG.exception(e) - await notify.emit(Events.LOG_ERROR, title="Error Adding URL", message=str(e), to=sid) + await notify.emit( + Events.LOG_ERROR, data={"preset": item.preset}, title="Error Adding URL", message=str(e), to=sid + ) @route(RouteType.SOCKET, "item_cancel", "item_cancel") diff --git a/ui/app/components/NotificationForm.vue b/ui/app/components/NotificationForm.vue index a85267c5..bbe9584c 100644 --- a/ui/app/components/NotificationForm.vue +++ b/ui/app/components/NotificationForm.vue @@ -48,7 +48,7 @@ - + You can use this field to populate the data, using shared string. @@ -63,7 +63,7 @@ - + The notification target name, this is used to identify the target in the logs and notifications. @@ -81,11 +81,13 @@ required> - + - The URL to send the notification to. It can be regular http/https endpoint. - or - Apprise URL. + + The URL to send the notification to. It can be regular http/https endpoint. or Apprise + URL. + @@ -105,7 +107,7 @@ - + The request method to use when sending the notification. This can be any of the standard HTTP @@ -130,7 +132,7 @@ - + The request type to use when sending the notification. This can be JSON or @@ -140,7 +142,7 @@ -
+
- + Subscribe to the events you want to listen for. When the event is triggered, the notification will @@ -169,7 +171,43 @@
-
+
+
+ +
+
+ +
+ +
+ + + + Select the presets you want to listen for. If you select presets, only events that reference those + presets will trigger the notification. If no presets are selected, the notification will be sent + for all presets. + + +
+
+ +
- + The field name to use when sending the notification. This is used to identify the data in the @@ -206,7 +244,7 @@
- + The header key to send with the notification. @@ -219,7 +257,7 @@ - + The header value to send with the notification. @@ -234,7 +272,7 @@ - + If header key or value is empty, the header will not be sent. @@ -274,6 +312,7 @@ import type { notification, notificationImport } from '~/types/notification' const emitter = defineEmits(['cancel', 'submit']) const toast = useNotification() const box = useConfirm() +const config = useConfigStore() const props = defineProps({ reference: { @@ -396,7 +435,17 @@ const importItem = async () => { if (item.on) { form.on = item.on + } + if (item.presets) { + item.presets.forEach(p => { + if (!config.presets.find(cp => cp.name === p)) { + return + } + if (!form.presets.includes(p)) { + form.presets.push(p) + } + }) } import_string.value = '' @@ -407,4 +456,5 @@ const importItem = async () => { } const isApprise = computed(() => form.request.url && !form.request.url.startsWith('http')) +const filter_presets = (flag: boolean = true) => config.presets.filter(item => item.default === flag) diff --git a/ui/app/pages/notifications.vue b/ui/app/pages/notifications.vue index 8855f943..11ad467f 100644 --- a/ui/app/pages/notifications.vue +++ b/ui/app/pages/notifications.vue @@ -46,7 +46,7 @@
- Send notifications to your servers based on specified events. + Send notifications to your webhooks based on specified events or presets.
@@ -76,15 +76,20 @@ -
+
{{ item.request.method.toUpperCase() }}({{ ucFirst(item.request.type) }}) @ - {{ item.name }} + {{ item.name }}
On: {{ join_events(item.on) }} +   + + + Presets: {{ join_presets(item.presets) }} +
@@ -139,6 +144,10 @@ On: {{ join_events(item.on) }}

+

+ + Presets: {{ join_presets(item.presets) }} +

{{item.request.headers.map(h => h.key).join(', ')}} @@ -186,14 +195,21 @@ to ensure that the exported data does not contain any sensitive information for sharing.

  • - When you set the request type as Form, the event data will be JSON encoded and sent as the - and sent as ...&data_key=json_string, only the data field will be JSON encoded. + When you set the request type as Form, the event data will be JSON encoded and sent as + ...&data_key=json_string, only the data field will be JSON encoded. The other keys id, event and created_at will be sent as they are.
  • We also send two special headers X-Event-ID and X-Event with the request.
  • Support for Apprise URLs is in beta and subject to many changes to come, currently the - message field fallback to JSON encoded string of the event if there there is custom message set by us for - that particular event.
  • + message field fallback to JSON encoded string of the event if no custom message set by us for that + particular event. +
  • + If you have selected specific presets or events, this will take priority, For example, if you limited the + target to default preset and selected ALL events, only events that reference the + default preset will be sent to that target. Like wise, if you have limited both events and + presets, then ONLY events that satisfy both conditions will be sent to that target. Only the + test events can bypass these conditions. +
  • @@ -213,17 +229,15 @@ const isMobile = useMediaQuery({ maxWidth: 1024 }) const allowedEvents = ref([]) const notifications = ref([]) -const target = ref({ + +const defaultState = (): notification => ({ name: '', on: [], - request: { - method: 'POST', - url: '', - type: 'json', - headers: [], - data_key: '', - }, + presets: [], + request: { method: 'POST', url: '', type: 'json', headers: [], data_key: 'data' }, }) + +const target = ref(defaultState()) const targetRef = ref('') const toggleForm = ref(false) const isLoading = ref(false) @@ -274,17 +288,7 @@ const reloadContent = async (fromMounted = false) => { } const resetForm = (closeForm = false) => { - target.value = { - name: '', - on: [], - request: { - method: 'POST', - url: '', - type: 'json', - headers: [], - data_key: '', - }, - } + target.value = defaultState() targetRef.value = null if (closeForm) { toggleForm.value = false @@ -331,13 +335,7 @@ const deleteItem = async (item: notification) => { toast.success('Notification target deleted.') } -const updateItem = async ({ - reference, - item, -}: { - reference: string | null; - item: notification; -}) => { +const updateItem = async ({ reference, item }: { reference: string | null, item: notification }) => { if (reference) { const index = notifications.value.findIndex(i => i?.id === reference) if (0 <= index) { @@ -369,8 +367,8 @@ const editItem = (item: notification) => { toggleForm.value = true } -const join_events = (events: string[]) => - !events || events.length < 1 ? 'ALL' : events.map(e => ucFirst(e)).join(', ') +const join_events = (events: Array) => !events || events.length < 1 ? 'ALL' : events.map(e => ucFirst(e)).join(', ') +const join_presets = (presets: Array) => !presets || presets.length < 1 ? 'ALL' : presets.map(e => ucFirst(e)).join(', ') const sendTest = async () => { if (true !== (await box.confirm('Send test notification?'))) { diff --git a/ui/app/types/notification.d.ts b/ui/app/types/notification.d.ts index 56e921dc..d34fba6b 100644 --- a/ui/app/types/notification.d.ts +++ b/ui/app/types/notification.d.ts @@ -15,7 +15,8 @@ type notification = { id?: string; name: string; request: notificationRequest; - on: string[]; + on: Array; + presets: Array; raw?: boolean; };