migrated all our emitter usage to EventBus

This commit is contained in:
ArabCoders 2025-03-22 02:05:52 +03:00
parent 07fc86565a
commit 4ed7c26bb4
13 changed files with 128 additions and 57 deletions

View file

@ -502,7 +502,7 @@ class DownloadQueue(metaclass=Singleton):
await item.close() await item.close()
LOG.debug(f"Deleting from queue {item_ref}") LOG.debug(f"Deleting from queue {item_ref}")
self.queue.delete(id) self.queue.delete(id)
await self._notify.emit(Events.CANCELLED, info=item.info.serialize()) await self._notify.emit(Events.CANCELLED, data=item.info.serialize())
item.info.status = "cancelled" item.info.status = "cancelled"
item.info.error = "Cancelled by user." item.info.error = "Cancelled by user."
self.done.put(item) self.done.put(item)

View file

@ -133,23 +133,21 @@ class Events:
""" """
return [ return [
Events.INITIAL_DATA,
Events.ADDED, Events.ADDED,
Events.UPDATED,
Events.COMPLETED,
Events.CANCELLED,
Events.CLEARED,
Events.ERROR, Events.ERROR,
Events.LOG_INFO, Events.LOG_INFO,
Events.LOG_SUCCESS, Events.LOG_SUCCESS,
Events.INITIAL_DATA, Events.COMPLETED,
Events.YTDLP_CONVERT, Events.CANCELLED,
Events.ITEM_DELETE, Events.CLEARED,
Events.ITEM_CANCEL, Events.UPDATED,
Events.UPDATE,
Events.PAUSED,
Events.PRESETS_UPDATE,
Events.STATUS, Events.STATUS,
Events.CLI_CLOSE, Events.CLI_CLOSE,
Events.CLI_OUTPUT, Events.CLI_OUTPUT,
Events.UPDATE,
Events.PAUSED,
] ]
@ -162,7 +160,7 @@ class Event:
id: str = field(default_factory=lambda: str(uuid.uuid4()), init=False) id: str = field(default_factory=lambda: str(uuid.uuid4()), init=False)
"""The id of the event.""" """The id of the event."""
created: str = field(default_factory=lambda: str(datetime.datetime.now(tz=datetime.timezone.utc).isoformat())) created_at: str = field(default_factory=lambda: str(datetime.datetime.now(tz=datetime.timezone.utc).isoformat()))
"""The time the event was created.""" """The time the event was created."""
event: str event: str
@ -179,10 +177,10 @@ class Event:
dict: The serialized event. dict: The serialized event.
""" """
return {"id": self.id, "created": self.created, "event": self.event, "data": self.data} return {"id": self.id, "created_at": self.created_at, "event": self.event, "data": self.data}
def __repr__(self): def __repr__(self):
return f"Event(id={self.id}, created={self.created}, event={self.event}, data={self.data})" return f"Event(id={self.id}, created_at={self.created_at}, event={self.event}, data={self.data})"
def datatype(self) -> str: def datatype(self) -> str:
""" """
@ -195,7 +193,7 @@ class Event:
return type(self.data).__name__ return type(self.data).__name__
def __str__(self): def __str__(self):
return f"Event(id={self.id}, created={self.created}, event={self.event})" return f"Event(id={self.id}, created_at={self.created_at}, event={self.event})"
class EventListener: class EventListener:
@ -250,7 +248,7 @@ class EventBus(metaclass=Singleton):
Args: Args:
event (str): The event to subscribe to. event (str): The event to subscribe to.
name (str|None): The name of the subscriber, if None a random uuid will be generated. name (str|None): The name of the subscriber, if None a random uuid will be generated.
callback(Event) (Awaitable): The function to call. Must be a coroutine. callback(Event, name, **kwargs) (Awaitable): The function to call. Must be a coroutine.
Returns: Returns:
EventsSubscriber: The instance of the EventsSubscriber EventsSubscriber: The instance of the EventsSubscriber

View file

@ -78,7 +78,9 @@ class HttpSocket(Common):
self.sio.on(method._ws_event)(method) # type: ignore self.sio.on(method._ws_event)(method) # type: ignore
self._notify.subscribe( self._notify.subscribe(
Events.ADD_URL, lambda data, _: self.add(**data.data), f"{__class__.__name__}.socket_add_url" Events.ADD_URL,
lambda data, _, **kwargs: self.add(**data.data), # noqa: ARG005
f"{__class__.__name__}.socket_add_url",
) )
# register the shutdown event. # register the shutdown event.

View file

@ -223,7 +223,7 @@ class Notification(metaclass=Singleton):
self._targets.append(target) self._targets.append(target)
LOG.info( LOG.info(
f"Will send '{target.on if len(target.on) > 0 else 'all'}' as {target.request.type} notification events to '{target.name}'." f"Will send {target.request.type} request on '{', '.join(target.on) if len(target.on) > 0 else 'all events'}' to '{target.name}'."
) )
except Exception as e: except Exception as e:
LOG.error(f"Error loading notification target '{target}'. '{e!s}'") LOG.error(f"Error loading notification target '{target}'. '{e!s}'")
@ -323,37 +323,35 @@ class Notification(metaclass=Singleton):
return True return True
async def send(self, event: str, item: ItemDTO | dict) -> list[dict]: async def send(self, ev: Event) -> list[dict]:
if len(self._targets) < 1: if len(self._targets) < 1:
return [] return []
if not isinstance(item, ItemDTO) and not isinstance(item, dict): if not isinstance(ev.data, ItemDTO) and not isinstance(ev.data, dict):
LOG.debug(f"Received invalid item type '{type(item)}' with event '{event}'.") LOG.debug(f"Received invalid item type '{type(ev.data)}' with event '{ev.event}'.")
return [] return []
tasks = [] tasks = []
for target in self._targets: for target in self._targets:
if len(target.on) > 0 and event not in target.on and "test" != event: if len(target.on) > 0 and ev.event not in target.on and "test" != ev.event:
continue continue
tasks.append(self._send(event, target, item)) tasks.append(self._send(target, ev))
return await asyncio.gather(*tasks) return await asyncio.gather(*tasks)
async def _send(self, event: str, target: Target, item: ItemDTO | dict) -> dict: async def _send(self, target: Target, ev: Event) -> dict:
try: try:
itemId = item.get("id", item.get("_id", "??")) LOG.info(f"Sending Notification event '{ev.event}: {ev.id}' to '{target.name}'.")
except Exception:
itemId = "??"
try:
LOG.info(f"Sending Notification event '{event}' id '{itemId}' to '{target.name}'.")
reqBody = { reqBody = {
"method": target.request.method.upper(), "method": target.request.method.upper(),
"url": target.request.url, "url": target.request.url,
"headers": { "headers": {
"User-Agent": f"YTPTube/{APP_VERSION}", "User-Agent": f"YTPTube/{APP_VERSION}",
"X-Event-Id": ev.id,
"X-Event": ev.event,
"Content-Type": "application/json" "Content-Type": "application/json"
if "json" == target.request.type.lower() if "json" == target.request.type.lower()
else "application/x-www-form-urlencoded", else "application/x-www-form-urlencoded",
@ -364,20 +362,16 @@ class Notification(metaclass=Singleton):
for h in target.request.headers: for h in target.request.headers:
reqBody["headers"][h.key] = h.value reqBody["headers"][h.key] = h.value
reqBody["json" if "json" == target.request.type.lower() else "data"] = { reqBody["json" if "json" == target.request.type.lower() else "data"] = self._deep_unpack(ev.serialize())
"event": event,
"created_at": datetime.now(tz=UTC).isoformat(),
"payload": item.__dict__ if isinstance(item, ItemDTO) else item,
}
if "form" == target.request.type.lower(): if "form" == target.request.type.lower():
reqBody["data"]["payload"] = self._encoder.encode(reqBody["data"]["payload"]) reqBody["data"]["data"] = self._encoder.encode(reqBody["data"]["data"])
response = await self._client.request(**reqBody) response = await self._client.request(**reqBody)
respData = {"url": target.request.url, "status": response.status_code, "text": response.text} respData = {"url": target.request.url, "status": response.status_code, "text": response.text}
msg = f"Notification target '{target.name}' Responded to event '{event}' id '{itemId}' with status '{response.status_code}'." msg = f"Notification target '{target.name}' Responded to event '{ev.event}: {ev.id}' with status '{response.status_code}'."
if self._debug and respData.get("text"): if self._debug and respData.get("text"):
msg += f" body '{respData.get('text','??')}'." msg += f" body '{respData.get('text','??')}'."
@ -385,14 +379,28 @@ class Notification(metaclass=Singleton):
return respData return respData
except Exception as e: except Exception as e:
LOG.error(f"Error sending Notification event '{event}' id '{itemId}' to '{target.name}'. '{e}'.") LOG.exception(e)
return {"url": target.request.url, "status": 500, "text": str(e)} LOG.error(f"Error sending Notification event '{ev.event}: {ev.id}' to '{target.name}'. '{e!s}'.")
return {"url": target.request.url, "status": 500, "text": str(ev)}
def emit(self, e: Event, _, **kwargs): # noqa: ARG002 def emit(self, e: Event, _, **kwargs): # noqa: ARG002
if len(self._targets) < 1: if len(self._targets) < 1:
return False return []
if not NotificationEvents.is_valid(e.event): if not NotificationEvents.is_valid(e.event):
return False return []
return self.send(e.event, e.data) return self.send(e)
def _deep_unpack(self, data: dict) -> dict:
for k, v in data.items():
if isinstance(v, dict):
data[k] = self._deep_unpack(v)
if isinstance(v, list):
data[k] = [self._deep_unpack(i) for i in v]
if isinstance(v, datetime):
data[k] = v.isoformat()
if isinstance(v, ItemDTO):
data[k] = v.serialize()
return data

View file

@ -83,7 +83,9 @@ class Presets(metaclass=Singleton):
self._default_presets = [Preset(**preset) for preset in json.load(f)] self._default_presets = [Preset(**preset) for preset in json.load(f)]
EventBus.get_instance().subscribe( EventBus.get_instance().subscribe(
Events.PRESETS_ADD, lambda data, _: self.add(**data.data), f"{__class__.__name__}.save" Events.PRESETS_ADD,
lambda data, _, **kwargs: self.add(**data.data), # noqa: ARG005
f"{__class__.__name__}.save",
) )
@staticmethod @staticmethod

View file

@ -27,7 +27,9 @@ class Scheduler(metaclass=Singleton):
self._loop = loop or asyncio.get_event_loop() self._loop = loop or asyncio.get_event_loop()
EventBus.get_instance().subscribe( EventBus.get_instance().subscribe(
Events.SCHEDULE_ADD, lambda data, _: self.add(**data.data), f"{__class__.__name__}.add" Events.SCHEDULE_ADD,
lambda data, _, **kwargs: self.add(**data.data), # noqa: ARG005
f"{__class__.__name__}.add",
) )
@staticmethod @staticmethod

View file

@ -80,8 +80,6 @@ class Tasks(metaclass=Singleton):
except Exception: except Exception:
pass pass
self._notify.subscribe(Events.TASKS_ADD, lambda data, _: self.add(**data.data), f"{__class__.__name__}.save")
@staticmethod @staticmethod
def get_instance() -> "Tasks": def get_instance() -> "Tasks":
""" """
@ -108,6 +106,11 @@ class Tasks(metaclass=Singleton):
""" """
self.load() self.load()
self._notify.subscribe(
Events.TASKS_ADD,
lambda data, _, **kwargs: self.add(**data.data), # noqa: ARG005
f"{__class__.__name__}.save",
)
def get_all(self) -> list[Task]: def get_all(self) -> list[Task]:
"""Return the tasks.""" """Return the tasks."""

View file

@ -5,6 +5,8 @@ from pathlib import Path
from yt_dlp.networking.impersonate import ImpersonateTarget from yt_dlp.networking.impersonate import ImpersonateTarget
from yt_dlp.utils import DateRange from yt_dlp.utils import DateRange
from .ItemDTO import ItemDTO
class Encoder(json.JSONEncoder): class Encoder(json.JSONEncoder):
""" """
@ -26,6 +28,9 @@ class Encoder(json.JSONEncoder):
if isinstance(o, ImpersonateTarget): if isinstance(o, ImpersonateTarget):
return str(o) return str(o)
if isinstance(o, ItemDTO):
return o.serialize()
if isinstance(o, object): if isinstance(o, object):
if hasattr(o, "serialize"): if hasattr(o, "serialize"):
return o.serialize() return o.serialize()

View file

@ -411,7 +411,7 @@ const setIcon = item => {
} }
const setIconColor = item => { const setIconColor = item => {
if (item.status === 'finished') { if ('finished' === item.status) {
return 'has-text-success' return 'has-text-success'
} }

View file

@ -76,11 +76,10 @@
</div> </div>
<div class="column is-half-mobile has-text-centered is-text-overflow"> <div class="column is-half-mobile has-text-centered is-text-overflow">
<span class="icon-text"> <span class="icon-text">
<span class="icon" :class="{ 'has-text-success': item.status == 'downloading' }"> <span class="icon" :class="setIconColor(item)">
<i class="fas" :class="setIcon(item)" /> <i class="fas fa-solid" :class="setIcon(item)" />
</span> </span>
<span v-if="item.status == 'downloading' && item.is_live">Live Streaming</span> <span v-text="setStatus(item)" />
<span v-else>{{ ucFirst(item.status) }}</span>
</span> </span>
</div> </div>
<div class="column is-half-mobile has-text-centered is-text-overflow"> <div class="column is-half-mobile has-text-centered is-text-overflow">
@ -153,7 +152,7 @@
<script setup> <script setup>
import moment from 'moment' import moment from 'moment'
import { useStorage } from '@vueuse/core' import { set, useStorage } from '@vueuse/core'
import { ucFirst } from '~/utils/index' import { ucFirst } from '~/utils/index'
import { isEmbedable, getEmbedable } from '~/utils/embedable' import { isEmbedable, getEmbedable } from '~/utils/embedable'
@ -184,24 +183,62 @@ const hasQueuedItems = computed(() => stateStore.count('queue') > 0)
const setIcon = item => { const setIcon = item => {
if ('downloading' === item.status && item.is_live) { if ('downloading' === item.status && item.is_live) {
return 'fa-solid fa-globe'; return 'fa-globe fa-spin';
} }
if ('downloading' === item.status) { if ('downloading' === item.status) {
return 'fa-solid fa-circle-check'; return 'fa-download';
} }
if ('postprocessing' === item.status) { if ('postprocessing' === item.status) {
return 'fa-solid fa-cog fa-spin'; return 'fa-cog fa-spin';
} }
if (null === item.status && true === config.paused) { if (null === item.status && true === config.paused) {
return 'fa-solid fa-pause-circle'; return 'fa-pause-circle';
} }
return 'fa-solid fa-spinner fa-spin'; if (!item.status) {
return 'fa-question';
}
return 'fa-spinner fa-spin';
} }
const setStatus = item => {
if (null === item.status && true === config.paused) {
return 'Paused';
}
if ('downloading' === item.status && item.is_live) {
return 'Live Streaming';
}
if (!item.status) {
return 'Unknown..';
}
return ucFirst(item.status)
}
const setIconColor = item => {
if (item.status === 'downloading') {
return 'has-text-success'
}
if ('postprocessing' === item.status) {
return 'has-text-info'
}
if (null === item.status && true === config.paused) {
return 'has-text-warning'
}
return ''
}
const ETAPipe = value => { const ETAPipe = value => {
if (value === null || 0 === value) { if (value === null || 0 === value) {
return 'Live'; return 'Live';

View file

@ -103,6 +103,14 @@ const runCommand = async () => {
return return
} }
if (command.value.startsWith('yt-dlp')) {
command.value = command.value.replace(/^yt-dlp/, '').trim()
await nextTick()
if ('' === command.value) {
return
}
}
if (!terminal.value) { if (!terminal.value) {
terminal.value = new Terminal({ terminal.value = new Terminal({
fontSize: 14, fontSize: 14,

View file

@ -124,6 +124,12 @@ div.is-centered {
However this might not be enough to remove credentials from the exported data. it's your responsibility However this might not be enough to remove credentials from the exported data. it's your responsibility
to ensure that the exported data does not contain any sensitive information for sharing. to ensure that the exported data does not contain any sensitive information for sharing.
</li> </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=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>
</ul> </ul>
</Message> </Message>
</div> </div>

View file

@ -70,7 +70,7 @@ export const useSocketStore = defineStore('socket', () => {
return return
} }
toast.info(`Download cancelled: ${ag(stateStore.get('queue', id, {}), id)}`); toast.warning(`Download cancelled: ${ag(stateStore.get('queue', id, {}), 'title')}`);
if (true === stateStore.has('queue', id)) { if (true === stateStore.has('queue', id)) {
stateStore.remove('queue', id); stateStore.remove('queue', id);