Merge pull request #405 from arabcoders/dev
Some checks failed
Build Native wrappers / build (amd64, ubuntu-latest) (push) Has been cancelled
Build Native wrappers / build (amd64, windows-latest) (push) Has been cancelled
Build Native wrappers / build (arm64, macos-latest) (push) Has been cancelled
Build Native wrappers / build (arm64, ubuntu-latest) (push) Has been cancelled
Build Native wrappers / build (arm64, windows-latest) (push) Has been cancelled

FEAT: Make it possible to limit notify target to specific presets.
This commit is contained in:
Abdulmohsen 2025-09-02 22:59:22 +03:00 committed by GitHub
commit c528e2b557
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 197 additions and 76 deletions

View file

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

View file

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

View file

@ -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}'",
)

View file

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

View file

@ -48,7 +48,7 @@
</button>
</div>
</div>
<span class="help">
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>You can use this field to populate the data, using shared string.</span>
</span>
@ -63,7 +63,7 @@
<input type="text" class="input" id="name" v-model="form.name" :disabled="addInProgress" required>
<span class="icon is-small is-left"><i class="fa-solid fa-user" /></span>
</div>
<span class="help">
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>The notification target name, this is used to identify the target in the logs and
notifications.</span>
@ -81,11 +81,13 @@
required>
<span class="icon is-small is-left"><i class="fa-solid fa-link" /></span>
</div>
<span class="help">
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span class="is-bold">The URL to send the notification to. It can be regular http/https endpoint.
or <NuxtLink target="blank" href="https://github.com/caronc/apprise?tab=readme-ov-file#readme">
Apprise</NuxtLink> URL.</span>
<span>
The URL to send the notification to. It can be regular http/https endpoint. or <NuxtLink
target="blank" href="https://github.com/caronc/apprise?tab=readme-ov-file#readme">Apprise
</NuxtLink> URL.
</span>
</span>
</div>
</div>
@ -105,7 +107,7 @@
</div>
<span class="icon is-small is-left"><i class="fa-solid fa-tv" /></span>
</div>
<span class="help">
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>
The request method to use when sending the notification. This can be any of the standard HTTP
@ -130,7 +132,7 @@
</div>
<span class="icon is-small is-left"><i class="fa-solid fa-tv" /></span>
</div>
<span class="help">
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>
The request type to use when sending the notification. This can be <code>JSON</code> or
@ -140,7 +142,7 @@
</div>
</div>
<div class="column is-12-mobile" :class="{ 'is-6-tablet': !isApprise, 'is-12': isApprise }">
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="on">
Select Events
@ -158,7 +160,7 @@
</div>
<span class="icon is-small is-left"><i class="fa-solid fa-paper-plane" /></span>
</div>
<span class="help">
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>
Subscribe to the events you want to listen for. When the event is triggered, the notification will
@ -169,7 +171,43 @@
</div>
</div>
<div class="column is-6-tablet is-12-mobile" v-if="!isApprise">
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="on">
Select Presets
<template v-if="form.presets.length > 0">
- <NuxtLink @click="form.presets = []">Clear selection</NuxtLink>
</template>
</label>
<div class="control has-icons-left">
<div class="select is-multiple is-fullwidth">
<select id="on" class="is-fullwidth" v-model="form.presets" :disabled="addInProgress" multiple>
<optgroup label="Custom presets" v-if="config?.presets.filter(p => !p?.default).length > 0">
<option v-for="item in filter_presets(false)" :key="item.id" :value="item.name">
{{ item.name }}
</option>
</optgroup>
<optgroup label="Default presets">
<option v-for="item in filter_presets(true)" :key="item.id" :value="item.name">
{{ item.name }}
</option>
</optgroup>
</select>
</div>
<span class="icon is-small is-left"><i class="fa-solid fa-sliders" /></span>
</div>
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>
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.
</span>
</span>
</div>
</div>
<div class="column is-12" v-if="!isApprise">
<div class="field">
<label class="label is-inline" for="data_key">
Data field
@ -179,7 +217,7 @@
:disabled="addInProgress" required>
<span class="icon is-small is-left"><i class="fa-solid fa-key" /></span>
</div>
<span class="help">
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>
The field name to use when sending the notification. This is used to identify the data in the
@ -206,7 +244,7 @@
<span class="icon is-small is-left"><i class="fa-solid fa-key" /></span>
</div>
</div>
<span class="help">
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>The header key to send with the notification.</span>
</span>
@ -219,7 +257,7 @@
<span class="icon is-small is-left"><i class="fa-solid fa-v" /></span>
</div>
</div>
<span class="help">
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>The header value to send with the notification.</span>
</span>
@ -234,7 +272,7 @@
</div>
</template>
</div>
<span class="help">
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-exclamation" /></span>
<span class="has-text-danger">
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)
</script>

View file

@ -46,7 +46,7 @@
</div>
<div class="is-hidden-mobile">
<span class="subtitle">
Send notifications to your servers based on specified events.
Send notifications to your webhooks based on specified events or presets.
</span>
</div>
</div>
@ -76,15 +76,20 @@
<tbody>
<tr v-for="item in notifications" :key="item.id">
<td class="is-text-overflow is-vcentered">
<div>
<div class="is-bold">
{{ item.request.method.toUpperCase() }}({{ ucFirst(item.request.type) }}) @
<NuxtLink target="_blank" :href="item.request.url">{{ item.name }}</NuxtLink>
<NuxtLink target="_blank" :href="item.request.url">{{ item.name }}</NuxtLink>
</div>
<div class="is-unselectable">
<span class="icon-text">
<span class="icon"><i class="fa-solid fa-list-ul" /></span>
<span>On: {{ join_events(item.on) }}</span>
</span>
&nbsp;
<span class="icon-text">
<span class="icon"><i class="fa-solid fa-sliders" /></span>
<span>Presets: {{ join_presets(item.presets) }}</span>
</span>
</div>
</td>
<td class="is-vcentered is-items-center">
@ -139,6 +144,10 @@
<span class="icon"><i class="fa-solid fa-list-ul" /></span>
<span>On: {{ join_events(item.on) }}</span>
</p>
<p>
<span class="icon"><i class="fa-solid fa-sliders" /></span>
<span>Presets: {{ join_presets(item.presets) }}</span>
</p>
<p v-if="item.request?.headers && item.request.headers.length > 0">
<span class="icon"><i class="fa-solid fa-heading" /></span>
<span>{{item.request.headers.map(h => h.key).join(', ')}}</span>
@ -186,14 +195,21 @@
to ensure that the exported data does not contain any sensitive information for sharing.
</li>
<li>
When you set the request type as <code>Form</code>, the event data will be JSON encoded and sent as the
and sent as <code>...&data_key=json_string</code>, only the <code>data</code> field will be JSON encoded.
When you set the request type as <code>Form</code>, the event data will be JSON encoded and sent as
<code>...&data_key=json_string</code>, only the <code>data</code> field will be JSON encoded.
The other keys <code>id</code>, <code>event</code> and <code>created_at</code> will be sent as they are.
</li>
<li>We also send two special headers <code>X-Event-ID</code> and <code>X-Event</code> with the request.</li>
<li>Support for <code>Apprise URLs</code> 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.</li>
message field fallback to JSON encoded string of the event if no custom message set by us for that
particular event.</li>
<li>
If you have selected specific presets or events, this will take priority, For example, if you limited the
target to <code>default</code> preset and selected <code>ALL</code> events, only events that reference the
<code>default</code> 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
<code>test</code> events can bypass these conditions.
</li>
</ul>
</Message>
</div>
@ -213,17 +229,15 @@ const isMobile = useMediaQuery({ maxWidth: 1024 })
const allowedEvents = ref<string[]>([])
const notifications = ref<notification[]>([])
const target = ref<notification>({
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<notification>(defaultState())
const targetRef = ref<string | null>('')
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<string>) => !events || events.length < 1 ? 'ALL' : events.map(e => ucFirst(e)).join(', ')
const join_presets = (presets: Array<string>) => !presets || presets.length < 1 ? 'ALL' : presets.map(e => ucFirst(e)).join(', ')
const sendTest = async () => {
if (true !== (await box.confirm('Send test notification?'))) {

View file

@ -15,7 +15,8 @@ type notification = {
id?: string;
name: string;
request: notificationRequest;
on: string[];
on: Array<string>;
presets: Array<string>;
raw?: boolean;
};