Merge pull request #342 from arabcoders/dev
Cleaning up the event dispatcher system.
This commit is contained in:
commit
e398321901
40 changed files with 648 additions and 429 deletions
|
|
@ -6,10 +6,16 @@ from queue import Empty, Queue
|
|||
|
||||
from .Singleton import Singleton
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
LOG: logging.Logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BackgroundWorker(metaclass=Singleton):
|
||||
"""
|
||||
Background worker to run tasks in a separate thread.
|
||||
This class uses a queue to submit tasks that will be executed in the background.
|
||||
It is designed to run in a separate thread and uses asyncio to handle asynchronous tasks.
|
||||
"""
|
||||
|
||||
_instance = None
|
||||
"""The instance of the Notification class."""
|
||||
|
||||
|
|
@ -37,19 +43,18 @@ class BackgroundWorker(metaclass=Singleton):
|
|||
except Exception as e:
|
||||
LOG.exception("Loop error: %s", e)
|
||||
|
||||
threading.Thread(target=_loop_runner, daemon=True).start()
|
||||
threading.Thread(target=_loop_runner, daemon=True, name="Background Runner").start()
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
fn, args, kwargs = self.queue.get(timeout=1)
|
||||
try:
|
||||
LOG.debug("Running background task: %s", fn.__name__)
|
||||
result = fn(*args, **kwargs)
|
||||
if inspect.iscoroutine(result):
|
||||
loop.call_soon_threadsafe(loop.create_task, result)
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
LOG.error(f"Error in background task: {fn.__name__}")
|
||||
LOG.error(f"Failed to run '{fn.__name__}'. {e!s}")
|
||||
except Empty:
|
||||
continue
|
||||
|
||||
|
|
|
|||
|
|
@ -492,10 +492,10 @@ class Download:
|
|||
if "error" == self.info.status and "error" in status:
|
||||
self.info.error = status.get("error")
|
||||
await self._notify.emit(
|
||||
Events.ERROR,
|
||||
data={"message": self.info.error, "data": self.info},
|
||||
Events.LOG_ERROR,
|
||||
data=self.info,
|
||||
title="Download Error",
|
||||
message=f"Error in download task '{self.info.title}': {self.info.error}",
|
||||
message=f"'{self.info.title}' failed to download: {self.info.error}",
|
||||
)
|
||||
|
||||
if "downloaded_bytes" in status and status.get("downloaded_bytes") > 0:
|
||||
|
|
|
|||
|
|
@ -19,8 +19,6 @@ from .config import Config
|
|||
from .DataStore import DataStore, StoreType
|
||||
from .Download import Download
|
||||
from .Events import EventBus, Events
|
||||
from .Events import info as event_info
|
||||
from .Events import warning as event_warning
|
||||
from .ItemDTO import Item, ItemDTO
|
||||
from .Presets import Presets
|
||||
from .Scheduler import Scheduler
|
||||
|
|
@ -452,39 +450,48 @@ class DownloadQueue(metaclass=Singleton):
|
|||
|
||||
try:
|
||||
dlInfo: Download = Download(info=dl, info_dict=entry if item.auto_start else None, logs=logs)
|
||||
notifyTitle: str | None = None
|
||||
notifyMessage: str | None = None
|
||||
|
||||
text_logs: str = ""
|
||||
if filtered_logs := extract_ytdlp_logs(logs):
|
||||
text_logs = f" Logs: {', '.join(filtered_logs)}"
|
||||
|
||||
if "is_upcoming" == entry.get("live_status"):
|
||||
NotifyEvent = Events.COMPLETED
|
||||
notifyEvent = Events.COMPLETED
|
||||
notifyTitle = "Upcoming Premiere" if is_premiere else "Upcoming Live Stream"
|
||||
notifyMessage = f"{'Premiere video' if is_premiere else 'Stream' } '{dlInfo.info.title}' is not available yet. {text_logs}"
|
||||
|
||||
dlInfo.info.status = "not_live"
|
||||
dlInfo.info.msg = f"{'Premiere video' if is_premiere else 'Stream' } is not available yet." + text_logs
|
||||
dlInfo.info.msg = notifyMessage.replace(f" '{dlInfo.info.title}'", "")
|
||||
|
||||
await self._notify.emit(
|
||||
Events.LOG_INFO,
|
||||
data=event_info(dlInfo.info.msg, {"lowPriority": True}),
|
||||
title="Premiere video" if is_premiere else "Live Stream",
|
||||
message=f"Item '{dlInfo.info.title}' is not available yet. {dlInfo.info.msg}",
|
||||
data={"lowPriority": True},
|
||||
title=notifyTitle,
|
||||
message=notifyMessage,
|
||||
)
|
||||
|
||||
itemDownload: Download = self.done.put(dlInfo)
|
||||
elif len(entry.get("formats", [])) < 1:
|
||||
availability: str = entry.get("availability", "public")
|
||||
msg: str = "No formats found."
|
||||
if availability and availability not in ("public",):
|
||||
msg += f" Availability is set for '{availability}'."
|
||||
ava: str = entry.get("availability", "public")
|
||||
notifyTitle = "Download Error"
|
||||
notifyMessage: str = f"No formats for '{dl.title}'."
|
||||
if ava and ava not in ("public",):
|
||||
notifyMessage += f" Availability is set for '{ava}'."
|
||||
|
||||
dlInfo.info.error = msg + text_logs
|
||||
dlInfo.info.error = notifyMessage.replace(f" for '{dl.title}'.", ".") + text_logs
|
||||
dlInfo.info.status = "error"
|
||||
itemDownload = self.done.put(dlInfo)
|
||||
NotifyEvent = Events.COMPLETED
|
||||
notifyEvent = Events.COMPLETED
|
||||
await self._notify.emit(
|
||||
Events.LOG_WARNING,
|
||||
data=event_warning(f"No formats found for '{dl.title}'."),
|
||||
title="No Formats Found",
|
||||
message=f"No formats found for '{dl.title}'. {dlInfo.info.error}",
|
||||
data={"logs": text_logs},
|
||||
title=notifyTitle,
|
||||
message=notifyMessage,
|
||||
)
|
||||
elif is_premiere and self.config.prevent_live_premiere:
|
||||
notifyTitle = "Premiere Video"
|
||||
dlInfo.info.error = "Premiering right now."
|
||||
|
||||
_requeue = True
|
||||
|
|
@ -496,6 +503,7 @@ class DownloadQueue(metaclass=Singleton):
|
|||
)
|
||||
starts_in = starts_in + timedelta(minutes=5, seconds=dl.extras.get("duration", 0))
|
||||
dlInfo.info.error += f" Download will start at {starts_in.astimezone().isoformat()}."
|
||||
notifyMessage = dlInfo.info.error.strip()
|
||||
_requeue = False
|
||||
except Exception as e:
|
||||
LOG.error(f"Failed to parse live_in date '{release_in}'. {e!s}")
|
||||
|
|
@ -503,25 +511,24 @@ class DownloadQueue(metaclass=Singleton):
|
|||
else:
|
||||
dlInfo.info.error += f" Delaying download by '{300+dl.extras.get('duration',0)}' seconds."
|
||||
|
||||
notifyMessage = dlInfo.info.error.strip()
|
||||
|
||||
if _requeue:
|
||||
NotifyEvent = Events.ADDED
|
||||
notifyEvent = Events.ADDED
|
||||
itemDownload = self.queue.put(dlInfo)
|
||||
if item.auto_start:
|
||||
self.event.set()
|
||||
else:
|
||||
LOG.debug(f"Item {itemDownload.info.name()} is not set to auto-start.")
|
||||
else:
|
||||
dlInfo.info.status = "not_live"
|
||||
itemDownload = self.done.put(dlInfo)
|
||||
NotifyEvent = Events.COMPLETED
|
||||
await self._notify.emit(
|
||||
Events.LOG_INFO,
|
||||
data=event_info(f"'{dl.title}' is {dlInfo.info.error}.", {"lowPriority": True}),
|
||||
title="Item Not Live",
|
||||
message=f"Item '{dl.title}' is not live. {dlInfo.info.error}",
|
||||
)
|
||||
notifyEvent = Events.COMPLETED
|
||||
notifyTitle = "Item Not Live"
|
||||
notifyMessage = f"Item '{dlInfo.info.title}' is not live."
|
||||
await self._notify.emit(Events.LOG_INFO, title=notifyTitle, message=notifyMessage)
|
||||
else:
|
||||
NotifyEvent = Events.ADDED
|
||||
notifyEvent = Events.ADDED
|
||||
notifyTitle = "Item Added"
|
||||
notifyMessage = f"Item '{dlInfo.info.title}' has been added to the download queue."
|
||||
itemDownload = self.queue.put(dlInfo)
|
||||
if item.auto_start:
|
||||
self.event.set()
|
||||
|
|
@ -529,10 +536,10 @@ class DownloadQueue(metaclass=Singleton):
|
|||
LOG.debug(f"Item {itemDownload.info.name()} is not set to auto-start.")
|
||||
|
||||
await self._notify.emit(
|
||||
NotifyEvent,
|
||||
notifyEvent,
|
||||
data=itemDownload.info.serialize(),
|
||||
title="Item Added" if Events.ADDED == NotifyEvent else None,
|
||||
message=f"Added '{itemDownload.info.title}'." if NotifyEvent == Events.ADDED else None,
|
||||
title=notifyTitle,
|
||||
message=notifyMessage,
|
||||
)
|
||||
|
||||
return {"status": "ok"}
|
||||
|
|
@ -740,14 +747,18 @@ class DownloadQueue(metaclass=Singleton):
|
|||
self.queue.delete(id)
|
||||
await self._notify.emit(
|
||||
Events.CANCELLED,
|
||||
data=item.info.serialize(),
|
||||
data=item.info,
|
||||
title="Download Cancelled",
|
||||
message=f"Download '{item.info.title}' has been cancelled.",
|
||||
)
|
||||
item.info.status = "cancelled"
|
||||
# item.info.error = "Cancelled by user."
|
||||
self.done.put(item)
|
||||
await self._notify.emit(Events.COMPLETED, data=item.info.serialize())
|
||||
await self._notify.emit(
|
||||
Events.COMPLETED,
|
||||
data=item.info,
|
||||
title="Download Cancelled",
|
||||
message=f"Download '{item.info.title}' has been cancelled.",
|
||||
)
|
||||
LOG.info(f"Deleted from queue {item_ref}")
|
||||
|
||||
status[id] = "ok"
|
||||
|
|
@ -815,11 +826,13 @@ class DownloadQueue(metaclass=Singleton):
|
|||
LOG.error(f"Unable to remove '{itemRef}' local file '{filename}'. {e!s}")
|
||||
|
||||
self.done.delete(id)
|
||||
|
||||
_status: str = "Removed" if removed_files > 0 else "Cleared"
|
||||
await self._notify.emit(
|
||||
Events.CLEARED,
|
||||
data=item.info.serialize(),
|
||||
title="Download Cleared",
|
||||
message=f"Cleared download '{item.info.title}' from history.",
|
||||
data=item.info,
|
||||
title=f"Download {_status}",
|
||||
message=f"{_status} '{item.info.title}' from history.",
|
||||
)
|
||||
|
||||
msg = f"Deleted completed download '{itemRef}'."
|
||||
|
|
@ -857,6 +870,27 @@ class DownloadQueue(metaclass=Singleton):
|
|||
|
||||
return items
|
||||
|
||||
def get_item(self, id: str) -> Download | None:
|
||||
"""
|
||||
Get a specific item from the download queue or history.
|
||||
|
||||
Args:
|
||||
id (str): The ID of the item to retrieve.
|
||||
|
||||
Returns:
|
||||
Download | None: The requested item if found, otherwise None.
|
||||
|
||||
"""
|
||||
try:
|
||||
return self.queue.get(key=id)
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
try:
|
||||
return self.done.get(key=id)
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
async def _download_pool(self) -> None:
|
||||
"""
|
||||
Create a pool of workers to download the files.
|
||||
|
|
@ -931,25 +965,25 @@ class DownloadQueue(metaclass=Singleton):
|
|||
LOG.debug(f"Download Task '{id}' is completed. Removing from queue.")
|
||||
self.queue.delete(key=id)
|
||||
|
||||
notifyTitle: str | None = None
|
||||
notifyMessage: str | None = None
|
||||
|
||||
if entry.is_cancelled() is True:
|
||||
await self._notify.emit(
|
||||
Events.CANCELLED,
|
||||
data=entry.info.serialize(),
|
||||
data=entry.info,
|
||||
title="Download Cancelled",
|
||||
message=f"Download '{entry.info.title}' has been cancelled.",
|
||||
)
|
||||
entry.info.status = "cancelled"
|
||||
# entry.info.error = "Cancelled by user."
|
||||
notifyTitle = "Download Cancelled"
|
||||
notifyMessage = f"Download '{entry.info.title}' has been cancelled."
|
||||
elif entry.info.status == "finished":
|
||||
notifyTitle = "Download Completed"
|
||||
notifyMessage = f"Download '{entry.info.title}' has been completed."
|
||||
|
||||
self.done.put(value=entry)
|
||||
await self._notify.emit(
|
||||
Events.COMPLETED,
|
||||
data=entry.info.serialize(),
|
||||
title="Download Completed" if entry.info.status == "finished" else None,
|
||||
message=f"Download '{entry.info.title}' has been completed."
|
||||
if entry.info.status == "finished"
|
||||
else None,
|
||||
)
|
||||
await self._notify.emit(Events.COMPLETED, data=entry.info, title=notifyTitle, message=notifyMessage)
|
||||
else:
|
||||
LOG.warning(f"Download '{id}' not found in queue.")
|
||||
|
||||
|
|
|
|||
|
|
@ -6,85 +6,10 @@ from collections.abc import Awaitable
|
|||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
from .BackgroundWorker import BackgroundWorker
|
||||
from .Singleton import Singleton
|
||||
|
||||
LOG = logging.getLogger("Events")
|
||||
|
||||
|
||||
def error(msg: str, data: dict | None = None) -> dict:
|
||||
"""
|
||||
Create an error message.
|
||||
|
||||
Args:
|
||||
msg (str): The message.
|
||||
data (dict|None): The data to include in the message.
|
||||
|
||||
Returns:
|
||||
dict : The message wrapped in a dictionary.
|
||||
|
||||
"""
|
||||
return message("error", msg, data)
|
||||
|
||||
|
||||
def warning(msg: str, data: dict | None = None) -> dict:
|
||||
"""
|
||||
Create an error message.
|
||||
|
||||
Args:
|
||||
msg (str): The message.
|
||||
data (dict|None): The data to include in the message.
|
||||
|
||||
Returns:
|
||||
dict : The message wrapped in a dictionary.
|
||||
|
||||
"""
|
||||
return message("warning", msg, data)
|
||||
|
||||
|
||||
def info(msg: str, data: dict | None = None) -> dict:
|
||||
"""
|
||||
Create an info message.
|
||||
|
||||
Args:
|
||||
msg (str): The message.
|
||||
data (dict|None): The data to include in the message.
|
||||
|
||||
Returns:
|
||||
dict : The message wrapped in a dictionary.
|
||||
|
||||
"""
|
||||
return message("info", msg, data)
|
||||
|
||||
|
||||
def success(msg: str, data: dict | None = None) -> dict:
|
||||
"""
|
||||
Create a success message.
|
||||
|
||||
Args:
|
||||
msg (str): The message.
|
||||
data (dict|None): The data to include in the message.
|
||||
|
||||
Returns:
|
||||
dict : The message wrapped in a dictionary.
|
||||
|
||||
"""
|
||||
return message("success", msg, data)
|
||||
|
||||
|
||||
def message(type: str, message: str, data: dict | None = None) -> dict:
|
||||
"""
|
||||
Create a message.
|
||||
|
||||
Args:
|
||||
type (str): The type of the message.
|
||||
message (str): The message.
|
||||
data (dict|None): The data to include in the message.
|
||||
|
||||
Returns:
|
||||
dict : The message wrapped in a dictionary.
|
||||
|
||||
"""
|
||||
return {"type": type, "message": message, "data": data if data else {}}
|
||||
LOG: logging.Logger = logging.getLogger("Events")
|
||||
|
||||
|
||||
class Events:
|
||||
|
|
@ -98,30 +23,32 @@ class Events:
|
|||
SHUTDOWN = "shutdown"
|
||||
|
||||
ADDED = "added"
|
||||
UPDATE = "update"
|
||||
UPDATED = "updated"
|
||||
COMPLETED = "completed"
|
||||
CANCELLED = "cancelled"
|
||||
CLEARED = "cleared"
|
||||
ERROR = "error"
|
||||
CONNECTED = "connected"
|
||||
STATUS = "status"
|
||||
|
||||
LOG_INFO = "log_info"
|
||||
LOG_WARNING = "log_warning"
|
||||
LOG_ERROR = "log_error"
|
||||
LOG_SUCCESS = "log_success"
|
||||
|
||||
INITIAL_DATA = "initial_data"
|
||||
ITEM_DELETE = "item_delete"
|
||||
ITEM_CANCEL = "item_cancel"
|
||||
ITEM_ERROR = "item_error"
|
||||
STATUS = "status"
|
||||
CLI_CLOSE = "cli_close"
|
||||
CLI_OUTPUT = "cli_output"
|
||||
UPDATE = "update"
|
||||
|
||||
TEST = "test"
|
||||
ADD_URL = "add_url"
|
||||
|
||||
CLI_POST = "cli_post"
|
||||
PAUSED = "paused"
|
||||
|
||||
CLI_POST = "cli_post"
|
||||
CLI_CLOSE = "cli_close"
|
||||
CLI_OUTPUT = "cli_output"
|
||||
|
||||
TASKS_ADD = "task_add"
|
||||
TASK_DISPATCHED = "task_dispatched"
|
||||
TASK_FINISHED = "task_finished"
|
||||
|
|
@ -129,6 +56,7 @@ class Events:
|
|||
|
||||
PRESETS_ADD = "presets_add"
|
||||
PRESETS_UPDATE = "presets_update"
|
||||
|
||||
SCHEDULE_ADD = "schedule_add"
|
||||
|
||||
CONDITIONS_ADD = "conditions_add"
|
||||
|
|
@ -158,9 +86,8 @@ class Events:
|
|||
|
||||
"""
|
||||
return [
|
||||
Events.INITIAL_DATA,
|
||||
Events.CONNECTED,
|
||||
Events.ADDED,
|
||||
Events.ERROR,
|
||||
Events.LOG_INFO,
|
||||
Events.LOG_WARNING,
|
||||
Events.LOG_ERROR,
|
||||
|
|
@ -185,7 +112,7 @@ class Events:
|
|||
list: The list of debug events.
|
||||
|
||||
"""
|
||||
return [Events.UPDATED]
|
||||
return [Events.UPDATED, Events.CLI_OUTPUT]
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
|
|
@ -209,7 +136,7 @@ class Event:
|
|||
message: str | None = None
|
||||
"""The message of the event, if any."""
|
||||
|
||||
data: any
|
||||
data: Any
|
||||
"""The data that was passed to the event."""
|
||||
|
||||
def serialize(self) -> dict:
|
||||
|
|
@ -277,12 +204,16 @@ class EventBus(metaclass=Singleton):
|
|||
debug: bool = False
|
||||
"""Whether to log debug messages or not."""
|
||||
|
||||
_offload: BackgroundWorker
|
||||
"""The background worker to offload tasks to."""
|
||||
|
||||
def __init__(self):
|
||||
EventBus._instance = self
|
||||
|
||||
from .config import Config
|
||||
|
||||
self.debug = Config.get_instance().debug
|
||||
self._offload = BackgroundWorker.get_instance()
|
||||
|
||||
@staticmethod
|
||||
def get_instance() -> "EventBus":
|
||||
|
|
@ -368,13 +299,24 @@ class EventBus(metaclass=Singleton):
|
|||
|
||||
return self
|
||||
|
||||
def sync_emit(self, event: str, data: Any, loop=None, wait: bool = True, **kwargs):
|
||||
def sync_emit(
|
||||
self,
|
||||
event: str,
|
||||
data: Any | None = None,
|
||||
title: str | None = None,
|
||||
message: str | None = None,
|
||||
loop=None,
|
||||
wait: bool = True,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Emit event and (optionally) wait for results.
|
||||
|
||||
Args:
|
||||
event (str): The event to emit.
|
||||
data (Any): The data to pass to the event.
|
||||
data (Any|None): The data to pass to the event.
|
||||
title (str | None): The title of the event, if any.
|
||||
message (str | None): The message of the event, if any.
|
||||
loop (asyncio.AbstractEventLoop | None): The event loop to use. If None, the current running loop is used.
|
||||
wait (bool): Whether to wait for the results of the event handlers. Defaults to True.
|
||||
**kwargs: Additional keyword arguments to pass to the event
|
||||
|
|
@ -389,8 +331,11 @@ class EventBus(metaclass=Singleton):
|
|||
if event not in self._listeners:
|
||||
return []
|
||||
|
||||
if not data:
|
||||
data = {}
|
||||
|
||||
async def emit_all():
|
||||
ev = Event(event=event, data=data)
|
||||
ev = Event(event=event, title=title, message=message, data=data)
|
||||
LOG.debug(f"Emitting event '{ev.id}: {ev.event}'.", extra={"data": data})
|
||||
|
||||
res: list = []
|
||||
|
|
@ -428,14 +373,14 @@ class EventBus(metaclass=Singleton):
|
|||
return fut.result() if wait else fut
|
||||
|
||||
async def emit(
|
||||
self, event: str, data: Any, title: str | None = None, message: str | None = None, **kwargs
|
||||
self, event: str, data: Any | None = None, title: str | None = None, message: str | None = None, **kwargs
|
||||
) -> Awaitable:
|
||||
"""
|
||||
Emit an event.
|
||||
|
||||
Args:
|
||||
event (str): The event to emit.
|
||||
data (Any): The data to pass to the event.
|
||||
data (Any|None): The data to pass to the event.
|
||||
title (str | None): The title of the event, if any.
|
||||
message (str | None): The message of the event, if any.
|
||||
**kwargs: The keyword arguments to pass to the event.
|
||||
|
|
@ -447,7 +392,10 @@ class EventBus(metaclass=Singleton):
|
|||
if event not in self._listeners:
|
||||
return []
|
||||
|
||||
ev = Event(event=event, data=data, title=title, message=message)
|
||||
if not data:
|
||||
data = {}
|
||||
|
||||
ev = Event(event=event, title=title, message=message, data=data)
|
||||
|
||||
if self.debug or event not in Events.only_debug():
|
||||
LOG.debug(f"Emitting event '{ev.id}: {ev.event}'.", extra={"data": data})
|
||||
|
|
@ -461,3 +409,35 @@ class EventBus(metaclass=Singleton):
|
|||
LOG.error(f"Failed to emit event '{ev.event}' to '{handler.name}'. Error message '{e!s}'.")
|
||||
|
||||
return asyncio.gather(*tasks)
|
||||
|
||||
def offload(
|
||||
self, event: str, title: str | None = None, message: str | None = None, data: Any | None = None, **kwargs
|
||||
) -> None:
|
||||
"""
|
||||
Offload an dispatching event to a background worker.
|
||||
|
||||
Args:
|
||||
event (str): The event to offload.
|
||||
data (Any|None): The data to pass to the event.
|
||||
title (str | None): The title of the event, if any.
|
||||
message (str | None): The message of the event, if any.
|
||||
**kwargs: Additional keyword arguments to pass to the event.
|
||||
|
||||
"""
|
||||
if event not in self._listeners:
|
||||
return
|
||||
|
||||
if not data:
|
||||
data = {}
|
||||
|
||||
ev = Event(event=event, title=title, message=message, data=data)
|
||||
|
||||
if self.debug or event not in Events.only_debug():
|
||||
LOG.debug(f"Offloading event '{ev.id}: {ev.event}'.", extra={"data": data})
|
||||
|
||||
for handler in self._listeners[event].values():
|
||||
try:
|
||||
self._offload.submit(handler.handle, ev, **kwargs)
|
||||
except Exception as e:
|
||||
LOG.exception(e)
|
||||
LOG.error(f"Failed to offload event '{ev.event}' to '{handler.name}'. Error message '{e!s}'.")
|
||||
|
|
|
|||
|
|
@ -55,7 +55,7 @@ class HttpSocket:
|
|||
self.rootPath = root_path
|
||||
|
||||
def emit(e: Event, _, **kwargs):
|
||||
return self.sio.emit(event=e.event, data=encoder.encode(e.data), **kwargs)
|
||||
return self.sio.emit(event=e.event, data=encoder.encode(e), **kwargs)
|
||||
|
||||
services = Services.get_instance()
|
||||
services.add_all(
|
||||
|
|
|
|||
|
|
@ -92,7 +92,6 @@ class Target:
|
|||
class NotificationEvents:
|
||||
ADDED = Events.ADDED
|
||||
COMPLETED = Events.COMPLETED
|
||||
ERROR = Events.ERROR
|
||||
CANCELLED = Events.CANCELLED
|
||||
CLEARED = Events.CLEARED
|
||||
LOG_INFO = Events.LOG_INFO
|
||||
|
|
@ -308,10 +307,17 @@ class Notification(metaclass=Singleton):
|
|||
msg = "Invalid notification target. Invalid 'on' event list found."
|
||||
raise ValueError(msg)
|
||||
|
||||
removed_events = []
|
||||
all_events = NotificationEvents.get_events().values()
|
||||
for e in target["on"]:
|
||||
if e not in NotificationEvents.get_events().values():
|
||||
msg = f"Invalid notification target. Invalid event '{e}' found."
|
||||
raise ValueError(msg)
|
||||
if e not in all_events:
|
||||
removed_events.append(e)
|
||||
target["on"].remove(e)
|
||||
continue
|
||||
|
||||
if len(removed_events) > 0 and len(target["on"]) < 1:
|
||||
msg: str = f"Invalid notification target. Invalid events '{', '.join(removed_events)}' found."
|
||||
raise ValueError(msg)
|
||||
|
||||
if "headers" in target["request"]:
|
||||
if not isinstance(target["request"]["headers"], list):
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ from app.library.Services import Services
|
|||
|
||||
from .config import Config
|
||||
from .encoder import Encoder
|
||||
from .Events import EventBus, Events, error, success
|
||||
from .Events import EventBus, Events
|
||||
from .Scheduler import Scheduler
|
||||
from .Singleton import Singleton
|
||||
from .Utils import init_class, validate_url
|
||||
|
|
@ -325,7 +325,7 @@ class Tasks(metaclass=Singleton):
|
|||
"template": template,
|
||||
"cli": cli,
|
||||
},
|
||||
title=f"Task '{task.name}' started",
|
||||
title="Tasks",
|
||||
message=f"Task '{task.name}' started at '{timeNow}'",
|
||||
id=task.id,
|
||||
)
|
||||
|
|
@ -337,19 +337,16 @@ class Tasks(metaclass=Singleton):
|
|||
|
||||
await self._notify.emit(
|
||||
Events.LOG_SUCCESS,
|
||||
data=success(
|
||||
f"Task '{task.name}' completed in '{ended - started:.2f}' seconds.", data={"lowPriority": True}
|
||||
),
|
||||
title=f"Task '{task.name}' completed",
|
||||
message=f"Task '{task.name}' completed at '{timeNow}'",
|
||||
data={"lowPriority": True},
|
||||
title="Task completed",
|
||||
message=f"Task '{task.name}' completed in '{ended - started:.2f}'.",
|
||||
)
|
||||
except Exception as e:
|
||||
LOG.error(f"Failed to execute '{task.name}' at '{timeNow}'. '{e!s}'.")
|
||||
await self._notify.emit(
|
||||
Events.ERROR,
|
||||
data=error(f"Failed to execute '{task.name}' at '{timeNow}'. '{e!s}'."),
|
||||
title=f"Task '{task.name}' failed",
|
||||
message=str(e),
|
||||
Events.LOG_ERROR,
|
||||
title="Task failed",
|
||||
message=f"Failed to execute '{task.name}'. '{e!s}'",
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
23
app/main.py
23
app/main.py
|
|
@ -111,7 +111,12 @@ class Main:
|
|||
host = host or self._config.host
|
||||
port = port or self._config.port
|
||||
|
||||
EventBus.get_instance().sync_emit(Events.STARTUP, data={"app": self._app})
|
||||
EventBus.get_instance().sync_emit(
|
||||
Events.STARTUP,
|
||||
data={"app": self._app},
|
||||
title="Application Startup",
|
||||
message="The application is starting up.",
|
||||
)
|
||||
Scheduler.get_instance().attach(self._app)
|
||||
|
||||
self._socket.attach(self._app)
|
||||
|
|
@ -123,7 +128,12 @@ class Main:
|
|||
Notification.get_instance().attach(self._app)
|
||||
Conditions.get_instance().attach(self._app)
|
||||
|
||||
EventBus.get_instance().sync_emit(Events.LOADED, data={"app": self._app})
|
||||
EventBus.get_instance().sync_emit(
|
||||
Events.LOADED,
|
||||
data={"app": self._app},
|
||||
title="Application Loaded",
|
||||
message="The application has loaded all components.",
|
||||
)
|
||||
|
||||
def started(_):
|
||||
LOG.info("=" * 40)
|
||||
|
|
@ -132,7 +142,14 @@ class Main:
|
|||
|
||||
loop = asyncio.get_event_loop()
|
||||
|
||||
EventBus.get_instance().sync_emit(Events.STARTED, data={"app": self._app}, loop=loop, wait=False)
|
||||
EventBus.get_instance().sync_emit(
|
||||
Events.STARTED,
|
||||
data={"app": self._app},
|
||||
title="Application Started",
|
||||
message="The application has started successfully.",
|
||||
loop=loop,
|
||||
wait=False,
|
||||
)
|
||||
|
||||
if loop and self._config.debug:
|
||||
loop.set_debug(True)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ from aiohttp import web
|
|||
from aiohttp.web import Request, Response
|
||||
|
||||
from app.library.encoder import Encoder
|
||||
from app.library.Events import EventBus, Events, message
|
||||
from app.library.Events import EventBus, Events
|
||||
from app.library.Notifications import Notification, NotificationEvents
|
||||
from app.library.router import route
|
||||
from app.library.Utils import validate_uuid
|
||||
|
|
@ -104,8 +104,6 @@ async def notification_test(encoder: Encoder, notify: EventBus) -> Response:
|
|||
Response: The response object.
|
||||
|
||||
"""
|
||||
data = message("test", "This is a test notification.")
|
||||
await notify.emit(Events.TEST, title="Test Notification", message="This is a test notification.")
|
||||
|
||||
await notify.emit(Events.TEST, data=data, title="Test Notification", message="This is a test notification.")
|
||||
|
||||
return web.json_response(data=data, status=web.HTTPOk.status_code, dumps=encoder.encode)
|
||||
return web.json_response(data={}, status=web.HTTPOk.status_code, dumps=encoder.encode)
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import asyncio
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
import socketio
|
||||
|
||||
from app.library.config import Config
|
||||
from app.library.DownloadQueue import DownloadQueue
|
||||
from app.library.Events import EventBus, Events, error
|
||||
from app.library.Events import EventBus, Events
|
||||
from app.library.Presets import Presets
|
||||
from app.library.router import RouteType, route
|
||||
from app.library.Utils import tail_log
|
||||
|
|
@ -30,7 +31,13 @@ async def connect(config: Config, queue: DownloadQueue, notify: EventBus, sid: s
|
|||
|
||||
data["folders"] = [folder.name for folder in Path(config.download_path).iterdir() if folder.is_dir()]
|
||||
|
||||
await notify.emit(Events.INITIAL_DATA, data=data, to=sid)
|
||||
await notify.emit(
|
||||
Events.CONNECTED,
|
||||
data=data,
|
||||
title="Client connected",
|
||||
message=f"Client '{sid}' connected.",
|
||||
to=sid,
|
||||
)
|
||||
|
||||
|
||||
@route(RouteType.SOCKET, "disconnect", "socket_disconnect")
|
||||
|
|
@ -52,7 +59,7 @@ async def disconnect(sio: socketio.AsyncServer, sid: str, data: str = None):
|
|||
|
||||
|
||||
@route(RouteType.SOCKET, "subscribe", "socket_subscribe")
|
||||
async def subscribe(config: Config, notify: EventBus, sio: socketio.AsyncServer, sid: str, data: str):
|
||||
async def subscribe(config: Config, notify: EventBus, sio: socketio.AsyncServer, sid: str, data: str | Any):
|
||||
"""
|
||||
Subscribe to a specific event.
|
||||
|
||||
|
|
@ -64,8 +71,13 @@ async def subscribe(config: Config, notify: EventBus, sio: socketio.AsyncServer,
|
|||
data (str): The event to subscribe to.
|
||||
|
||||
"""
|
||||
if not isinstance(data, str):
|
||||
await notify.emit(Events.ERROR, data=error("Invalid event."), to=sid)
|
||||
if not isinstance(data, str) or not data:
|
||||
await notify.emit(
|
||||
Events.LOG_ERROR,
|
||||
title="Subscription Error",
|
||||
message="Invalid event type was expecting a string.",
|
||||
to=sid,
|
||||
)
|
||||
return
|
||||
|
||||
if data not in _Data.subscribers:
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import anyio
|
|||
|
||||
from app.library.config import Config
|
||||
from app.library.DownloadQueue import DownloadQueue
|
||||
from app.library.Events import EventBus, Events, error
|
||||
from app.library.Events import EventBus, Events
|
||||
from app.library.ItemDTO import Item
|
||||
from app.library.router import RouteType, route
|
||||
from app.library.Utils import is_downloaded
|
||||
|
|
@ -43,43 +43,77 @@ async def add_url(queue: DownloadQueue, notify: EventBus, sid: str, data: dict):
|
|||
url: str | None = data.get("url")
|
||||
|
||||
if not url:
|
||||
await notify.emit(Events.ERROR, data=error("No URL provided.", data={"unlock": True}), to=sid)
|
||||
await notify.emit(
|
||||
Events.LOG_ERROR,
|
||||
title="Invalid URL",
|
||||
message="Please provide a valid URL to add to the download queue.",
|
||||
to=sid,
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
status = await queue.add(item=Item.format(data))
|
||||
await notify.emit(
|
||||
event=Events.STATUS,
|
||||
data=await queue.add(item=Item.format(data)),
|
||||
title="Adding URL",
|
||||
message=f"Adding URL '{url}' to the download queue.",
|
||||
data=status,
|
||||
to=sid,
|
||||
)
|
||||
except ValueError as e:
|
||||
LOG.exception(e)
|
||||
await notify.emit(Events.ERROR, data=error(str(e)), to=sid)
|
||||
return
|
||||
await notify.emit(Events.LOG_ERROR, title="Error Adding URL", message=str(e), to=sid)
|
||||
|
||||
|
||||
@route(RouteType.SOCKET, "item_cancel", "item_cancel")
|
||||
async def item_cancel(queue: DownloadQueue, notify: EventBus, sid: str, data: str):
|
||||
if not data:
|
||||
await notify.emit(Events.ERROR, data=error("Invalid request."), to=sid)
|
||||
await notify.emit(
|
||||
Events.LOG_ERROR,
|
||||
title="Invalid Request",
|
||||
message="No item ID provided to cancel.",
|
||||
to=sid,
|
||||
)
|
||||
return
|
||||
|
||||
status: dict[str, str] = {}
|
||||
status = await queue.cancel([data])
|
||||
status.update({"identifier": data})
|
||||
|
||||
await notify.emit(Events.ITEM_CANCEL, data=status)
|
||||
await notify.emit(
|
||||
Events.ITEM_CANCEL, data=status, title="Item Cancelled", message=f"Item '{data}': has been cancelled."
|
||||
)
|
||||
|
||||
|
||||
@route(RouteType.SOCKET, "item_delete", "item_delete")
|
||||
async def item_delete(queue: DownloadQueue, notify: EventBus, sid: str, data: dict):
|
||||
if not data:
|
||||
await notify.emit(Events.ERROR, data=error("Invalid request."), to=sid)
|
||||
await notify.emit(
|
||||
Events.LOG_ERROR,
|
||||
title="Invalid Request",
|
||||
message="No item ID provided to delete.",
|
||||
to=sid,
|
||||
)
|
||||
return
|
||||
|
||||
id: str | None = data.get("id")
|
||||
if not id:
|
||||
await notify.emit(Events.ERROR, data=error("Invalid request."), to=sid)
|
||||
await notify.emit(
|
||||
Events.LOG_ERROR,
|
||||
title="Invalid Request",
|
||||
message="No item ID provided to delete.",
|
||||
to=sid,
|
||||
)
|
||||
return
|
||||
|
||||
item = queue.get_item(id=id)
|
||||
if not item:
|
||||
await notify.emit(
|
||||
Events.LOG_ERROR,
|
||||
title="Item Not Found",
|
||||
message=f"Item with ID '{id}' not found.",
|
||||
to=sid,
|
||||
)
|
||||
return
|
||||
|
||||
status: dict[str, str] = {}
|
||||
|
|
@ -87,7 +121,7 @@ async def item_delete(queue: DownloadQueue, notify: EventBus, sid: str, data: di
|
|||
status.update({"identifier": id})
|
||||
|
||||
await notify.emit(
|
||||
Events.ITEM_DELETE, data=status, title="Item Deleted", message=f"Item with ID '{id}' has been deleted."
|
||||
Events.ITEM_DELETE, data=status, title="Item Deleted", message=f"Item '{item.info.title}': has been deleted."
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -145,7 +179,12 @@ async def archive_item(config: Config, data: dict):
|
|||
@route(RouteType.SOCKET, "item_start", "item_start")
|
||||
async def item_start(queue: DownloadQueue, notify: EventBus, sid: str, data: list | str) -> None:
|
||||
if not data:
|
||||
await notify.emit(Events.ERROR, data=error("Invalid request."), to=sid)
|
||||
await notify.emit(
|
||||
Events.LOG_ERROR,
|
||||
title="Invalid Request",
|
||||
message="No items provided to start.",
|
||||
to=sid,
|
||||
)
|
||||
return
|
||||
|
||||
if isinstance(data, str):
|
||||
|
|
@ -157,7 +196,12 @@ async def item_start(queue: DownloadQueue, notify: EventBus, sid: str, data: lis
|
|||
@route(RouteType.SOCKET, "item_pause", "item_pause")
|
||||
async def item_pause(queue: DownloadQueue, notify: EventBus, sid: str, data: list | str) -> None:
|
||||
if not data:
|
||||
await notify.emit(Events.ERROR, data=error("Invalid request."), to=sid)
|
||||
await notify.emit(
|
||||
Events.LOG_ERROR,
|
||||
title="Invalid Request",
|
||||
message="No items provided to pause.",
|
||||
to=sid,
|
||||
)
|
||||
return
|
||||
|
||||
if isinstance(data, str):
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import os
|
|||
from typing import TYPE_CHECKING
|
||||
|
||||
from app.library.config import Config
|
||||
from app.library.Events import EventBus, Events, error
|
||||
from app.library.Events import EventBus, Events
|
||||
from app.library.router import RouteType, route
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
|
@ -17,7 +17,12 @@ LOG: logging.Logger = logging.getLogger(__name__)
|
|||
@route(RouteType.SOCKET, "cli_post", "socket_cli_post")
|
||||
async def cli_post(config: Config, notify: EventBus, sid: str, data: str):
|
||||
if not config.console_enabled:
|
||||
await notify.emit(Events.ERROR, data=error("Console is disabled."), to=sid)
|
||||
await notify.emit(
|
||||
Events.LOG_ERROR,
|
||||
title="Feature disabled",
|
||||
message="Console feature is disabled.",
|
||||
to=sid,
|
||||
)
|
||||
return
|
||||
|
||||
if not data:
|
||||
|
|
@ -29,6 +34,7 @@ async def cli_post(config: Config, notify: EventBus, sid: str, data: str):
|
|||
import shlex
|
||||
import subprocess # ignore
|
||||
|
||||
returncode: int = -1
|
||||
try:
|
||||
LOG.info(f"Cli command from client '{sid}'. '{data}'")
|
||||
|
||||
|
|
@ -36,16 +42,22 @@ async def cli_post(config: Config, notify: EventBus, sid: str, data: str):
|
|||
_env: dict[str, str] = os.environ.copy()
|
||||
_env.update(
|
||||
{
|
||||
"TERM": "xterm-256color",
|
||||
"LANG": "en_US.UTF-8",
|
||||
"SHELL": "/bin/bash",
|
||||
"LC_ALL": "en_US.UTF-8",
|
||||
"PWD": config.download_path,
|
||||
"FORCE_COLOR": "1",
|
||||
"PYTHONUNBUFFERED": "1",
|
||||
}
|
||||
)
|
||||
|
||||
if "nt" != os.name:
|
||||
_env.update(
|
||||
{
|
||||
"TERM": "xterm-256color",
|
||||
"LANG": "en_US.UTF-8",
|
||||
"LC_ALL": "en_US.UTF-8",
|
||||
"SHELL": "/bin/bash",
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
import pty
|
||||
|
||||
|
|
@ -126,10 +138,9 @@ async def cli_post(config: Config, notify: EventBus, sid: str, data: str):
|
|||
returncode: int = await proc.wait()
|
||||
|
||||
await read_task
|
||||
|
||||
await notify.emit(Events.CLI_CLOSE, data={"exitcode": returncode}, to=sid)
|
||||
except Exception as e:
|
||||
LOG.error(f"CLI execute exception was thrown for client '{sid}'.")
|
||||
LOG.exception(e)
|
||||
await notify.emit(Events.CLI_OUTPUT, data={"type": "stderr", "line": str(e)}, to=sid)
|
||||
await notify.emit(Events.CLI_CLOSE, data={"exitcode": -1}, to=sid)
|
||||
finally:
|
||||
await notify.emit(Events.CLI_CLOSE, data={"exitcode": returncode}, to=sid)
|
||||
|
|
|
|||
|
|
@ -336,3 +336,7 @@ hr {
|
|||
.fa-spin-10 {
|
||||
--fa-animation-iteration-count: 10;
|
||||
}
|
||||
|
||||
.Vue-Toastification__toast-body {
|
||||
user-select: none;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@
|
|||
<script setup lang="ts">
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import { decode } from '~/utils/importer'
|
||||
import type { ConditionItem, ImportedConditionItem } from '~/@types/conditions'
|
||||
import type { ConditionItem, ImportedConditionItem } from '~/types/conditions'
|
||||
|
||||
const emitter = defineEmits<{
|
||||
(e: 'cancel'): void
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
<footer class="modal-card-foot p-5">
|
||||
<div class="field is-grouped" style="width:100%">
|
||||
<div class="control is-expanded">
|
||||
<button class="button is-fullwidth" :class="confirm_button_color" @click="handleConfirm">
|
||||
<button ref="confirmBtn" class="button is-fullwidth" :class="confirm_button_color" @click="handleConfirm">
|
||||
{{ confirm_button_label }}
|
||||
</button>
|
||||
</div>
|
||||
|
|
@ -83,21 +83,25 @@ const emit = defineEmits<{
|
|||
(e: 'cancel'): void
|
||||
}>()
|
||||
|
||||
const confirmBtn = ref<HTMLButtonElement | null>(null)
|
||||
const selected = reactive<Record<string, boolean>>({})
|
||||
|
||||
watch(() => props.visible, visible => {
|
||||
if (visible && props.options) {
|
||||
watch(() => props.visible, async visible => {
|
||||
if (!visible) {
|
||||
return
|
||||
}
|
||||
|
||||
if (props.options) {
|
||||
for (const opt of props.options) {
|
||||
selected[opt.key] ??= false
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
function handleConfirm() {
|
||||
emit('confirm', { ...selected })
|
||||
}
|
||||
await nextTick()
|
||||
confirmBtn.value?.focus()
|
||||
}, { immediate: true })
|
||||
|
||||
function cancel() {
|
||||
emit('cancel')
|
||||
}
|
||||
|
||||
const handleConfirm = () => emit('confirm', { ...selected })
|
||||
const cancel = () => emit('cancel')
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -231,7 +231,7 @@
|
|||
<script setup lang="ts">
|
||||
import 'assets/css/bulma-switch.css'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import type { item_request } from '~/@types/item'
|
||||
import type { item_request } from '~/types/item'
|
||||
import { getSeparatorsName, separators } from '~/utils/utils'
|
||||
|
||||
const props = defineProps<{ item?: Partial<item_request> }>()
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ hr {
|
|||
|
||||
<script setup lang="ts">
|
||||
import moment from 'moment'
|
||||
import type { changelogs, changeset } from '~/@types/changelogs'
|
||||
import type { changelogs, changeset } from '~/types/changelogs'
|
||||
|
||||
const toast = useNotification()
|
||||
const config = useConfigStore()
|
||||
|
|
|
|||
|
|
@ -117,7 +117,7 @@
|
|||
<script setup lang="ts">
|
||||
import { request } from '~/utils/index'
|
||||
import { encode } from '~/utils/importer'
|
||||
import type { ConditionItem, ImportedConditionItem } from '~/@types/conditions'
|
||||
import type { ConditionItem, ImportedConditionItem } from '~/types/conditions'
|
||||
|
||||
const toast = useNotification()
|
||||
const config = useConfigStore()
|
||||
|
|
|
|||
|
|
@ -63,6 +63,7 @@ import { useStorage } from '@vueuse/core'
|
|||
|
||||
const config = useConfigStore()
|
||||
const socket = useSocketStore()
|
||||
const toast = useNotification()
|
||||
|
||||
const bg_enable = useStorage<boolean>('random_bg', true)
|
||||
const bg_opacity = useStorage<number>('random_bg_opacity', 0.95)
|
||||
|
|
@ -88,14 +89,15 @@ watch(() => config.app.basic_mode, async () => {
|
|||
return
|
||||
}
|
||||
await navigateTo('/')
|
||||
})
|
||||
}, { immediate: true })
|
||||
|
||||
watch(() => config.app.console_enabled, async () => {
|
||||
if (config.app.console_enabled) {
|
||||
return
|
||||
}
|
||||
toast.error('Console is disabled in the configuration. Please enable it to use this feature.')
|
||||
await navigateTo('/')
|
||||
})
|
||||
}, { immediate: true })
|
||||
|
||||
const handle_event = () => {
|
||||
if (!terminal.value) {
|
||||
|
|
@ -171,9 +173,9 @@ const writer = (s: string) => {
|
|||
return
|
||||
}
|
||||
|
||||
const data = JSON.parse(s)
|
||||
const json = JSON.parse(s)
|
||||
|
||||
terminal.value.writeln(data.line)
|
||||
terminal.value.writeln(json.data.line)
|
||||
}
|
||||
|
||||
const loader = () => isLoading.value = false
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import type { item_request } from '~/@types/item'
|
||||
import type { item_request } from '~/types/item'
|
||||
|
||||
const config = useConfigStore()
|
||||
const stateStore = useStateStore()
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ import moment from 'moment'
|
|||
import { request } from '~/utils/index'
|
||||
import { ref, onMounted, nextTick } from 'vue'
|
||||
import { useStorage } from '@vueuse/core'
|
||||
import type { log_line } from '~/@types/logs'
|
||||
import type { log_line } from '~/types/logs'
|
||||
|
||||
let scrollTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
|
|
|
|||
|
|
@ -330,7 +330,7 @@ import { useStorage } from '@vueuse/core'
|
|||
import { CronExpressionParser } from 'cron-parser'
|
||||
import { request, sleep } from '~/utils/index'
|
||||
import { encode } from '~/utils/importer'
|
||||
import type { task_item, exported_task, error_response } from '~/@types/tasks'
|
||||
import type { task_item, exported_task, error_response } from '~/types/tasks'
|
||||
|
||||
const box = useConfirm()
|
||||
const toast = useNotification()
|
||||
|
|
@ -686,7 +686,8 @@ const runNow = async (item: task_item, mass: boolean = false) => {
|
|||
onBeforeUnmount(() => socket.off('status', statusHandler))
|
||||
|
||||
const statusHandler = async (stream: string) => {
|
||||
const { status, msg } = JSON.parse(stream)
|
||||
const json = JSON.parse(stream)
|
||||
const { status, msg } = json.data
|
||||
|
||||
if ('error' === status) {
|
||||
toast.error(msg)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useStorage } from '@vueuse/core'
|
||||
import type { ConfigState } from '~/@types/config';
|
||||
import type { ConfigState } from '~/types/config';
|
||||
|
||||
export const useConfigStore = defineStore('config', () => {
|
||||
const state = reactive<ConfigState>({
|
||||
|
|
|
|||
|
|
@ -1,163 +0,0 @@
|
|||
import { io } from "socket.io-client";
|
||||
import { ag } from "~/utils/index"
|
||||
|
||||
export const useSocketStore = defineStore('socket', () => {
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const config = useConfigStore()
|
||||
const stateStore = useStateStore()
|
||||
const toast = useNotification()
|
||||
|
||||
const socket = ref(null)
|
||||
const isConnected = ref(false)
|
||||
|
||||
const connect = () => {
|
||||
let opts = {
|
||||
transports: ['websocket', 'polling'],
|
||||
withCredentials: true,
|
||||
}
|
||||
|
||||
let url = runtimeConfig.public.wss
|
||||
|
||||
if ('development' !== runtimeConfig.public?.APP_ENV) {
|
||||
url = window.origin;
|
||||
opts.path = `${runtimeConfig.app.baseURL.replace(/\/$/, '')}/socket.io`;
|
||||
}
|
||||
|
||||
socket.value = io(url, opts)
|
||||
|
||||
socket.value.on('connect', () => isConnected.value = true);
|
||||
socket.value.on('disconnect', () => isConnected.value = false);
|
||||
|
||||
socket.value.on('initial_data', stream => {
|
||||
const initialData = JSON.parse(stream)
|
||||
|
||||
config.setAll({
|
||||
app: initialData['config'],
|
||||
tasks: initialData['tasks'],
|
||||
folders: initialData['folders'],
|
||||
presets: initialData['presets'],
|
||||
paused: Boolean(initialData['paused'])
|
||||
})
|
||||
|
||||
stateStore.addAll('queue', initialData['queue'] ?? {})
|
||||
stateStore.addAll('history', initialData['done'] ?? {})
|
||||
})
|
||||
|
||||
socket.value.on('added', stream => {
|
||||
const item = JSON.parse(stream);
|
||||
stateStore.add('queue', item._id, item);
|
||||
toast.success(`Item queued: ${ag(stateStore.get('queue', item._id, {}), 'title')}`);
|
||||
});
|
||||
|
||||
socket.value.on('error', stream => {
|
||||
const json = JSON.parse(stream);
|
||||
toast.error(`${json.data?.id ?? json?.type}: ${json?.message}`, json.data || {});
|
||||
});
|
||||
|
||||
socket.value.on('log_info', stream => {
|
||||
const json = JSON.parse(stream);
|
||||
toast.info(json?.message, json.data || {});
|
||||
});
|
||||
|
||||
socket.value.on('log_success', stream => {
|
||||
const json = JSON.parse(stream);
|
||||
toast.success(json?.message, json.data || {});
|
||||
});
|
||||
|
||||
socket.value.on('log_warning', stream => {
|
||||
const json = JSON.parse(stream);
|
||||
toast.warning(json?.message, json.data || {});
|
||||
});
|
||||
|
||||
socket.value.on('log_error', stream => {
|
||||
const json = JSON.parse(stream);
|
||||
toast.error(json?.message, json.data || {});
|
||||
});
|
||||
|
||||
socket.value.on('completed', stream => {
|
||||
const item = JSON.parse(stream);
|
||||
|
||||
if (true === stateStore.has('queue', item._id)) {
|
||||
stateStore.remove('queue', item._id);
|
||||
}
|
||||
|
||||
stateStore.add('history', item._id, item);
|
||||
});
|
||||
|
||||
socket.value.on('cancelled', stream => {
|
||||
const item = JSON.parse(stream);
|
||||
const id = item._id
|
||||
|
||||
if (true !== stateStore.has('queue', id)) {
|
||||
return
|
||||
}
|
||||
|
||||
toast.warning(`Download cancelled: ${ag(stateStore.get('queue', id, {}), 'title')}`);
|
||||
|
||||
if (true === stateStore.has('queue', id)) {
|
||||
stateStore.remove('queue', id);
|
||||
}
|
||||
});
|
||||
|
||||
socket.value.on('cleared', stream => {
|
||||
const item = JSON.parse(stream);
|
||||
const id = item._id
|
||||
|
||||
if (true !== stateStore.has('history', id)) {
|
||||
return
|
||||
}
|
||||
|
||||
stateStore.remove('history', id);
|
||||
});
|
||||
|
||||
socket.value.on("updated", stream => {
|
||||
const data = JSON.parse(stream);
|
||||
|
||||
if (true === stateStore.has('history', data._id)) {
|
||||
stateStore.update('history', data._id, data);
|
||||
return;
|
||||
}
|
||||
|
||||
let dl = stateStore.get('queue', data._id, {});
|
||||
data.deleting = dl?.deleting;
|
||||
stateStore.update('queue', data._id, data);
|
||||
});
|
||||
|
||||
socket.value.on("update", stream => {
|
||||
const data = JSON.parse(stream);
|
||||
if (true === stateStore.has('history', data._id)) {
|
||||
stateStore.update('history', data._id, data);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
socket.value.on('paused', data => {
|
||||
const json = JSON.parse(data);
|
||||
const pausedState = Boolean(json.paused);
|
||||
config.update('paused', pausedState);
|
||||
|
||||
if (false === pausedState) {
|
||||
toast.success('Download queue resumed.');
|
||||
return;
|
||||
}
|
||||
|
||||
toast.warning('Download queue paused.', {
|
||||
timeout: 10000,
|
||||
});
|
||||
});
|
||||
|
||||
socket.value.on('presets_update', data => config.update('presets', JSON.parse(data)));
|
||||
}
|
||||
|
||||
const on = (event, callback) => socket.value.on(event, callback);
|
||||
const off = (event, callback) => socket.value.off(event, callback);
|
||||
const emit = (event, data) => socket.value.emit(event, data);
|
||||
|
||||
if (false === isConnected.value) {
|
||||
connect();
|
||||
}
|
||||
|
||||
window.ws = socket.value;
|
||||
|
||||
return { connect, on, off, emit, socket, isConnected };
|
||||
});
|
||||
157
ui/stores/SocketStore.ts
Normal file
157
ui/stores/SocketStore.ts
Normal file
|
|
@ -0,0 +1,157 @@
|
|||
import { io } from "socket.io-client";
|
||||
import { ag } from "~/utils/index"
|
||||
import type { Socket as IOSocket, SocketOptions } from "socket.io-client"
|
||||
import type { ManagerOptions } from "socket.io-client";
|
||||
|
||||
export const useSocketStore = defineStore('socket', () => {
|
||||
const runtimeConfig = useRuntimeConfig()
|
||||
const config = useConfigStore()
|
||||
const stateStore = useStateStore()
|
||||
const toast = useNotification()
|
||||
|
||||
const socket = ref<IOSocket | null>(null)
|
||||
const isConnected = ref<boolean>(false)
|
||||
|
||||
const connect = () => {
|
||||
let opts = {
|
||||
transports: ['websocket', 'polling'],
|
||||
withCredentials: true,
|
||||
} as Partial<ManagerOptions & SocketOptions>
|
||||
|
||||
let url = runtimeConfig.public.wss
|
||||
|
||||
if ('development' !== runtimeConfig.public?.APP_ENV) {
|
||||
url = window.origin;
|
||||
opts.path = `${runtimeConfig.app.baseURL.replace(/\/$/, '')}/socket.io`;
|
||||
}
|
||||
|
||||
socket.value = io(url, opts)
|
||||
|
||||
socket.value.on('connect', () => isConnected.value = true);
|
||||
socket.value.on('disconnect', () => isConnected.value = false);
|
||||
|
||||
socket.value.on('connected', stream => {
|
||||
const json = JSON.parse(stream)
|
||||
|
||||
config.setAll({
|
||||
app: json.data.config,
|
||||
tasks: json.data.tasks,
|
||||
folders: json.data.folders,
|
||||
presets: json.data.presets,
|
||||
paused: Boolean(json.data.paused)
|
||||
})
|
||||
|
||||
stateStore.addAll('queue', json.data.queue || {})
|
||||
stateStore.addAll('history', json.data.done || {})
|
||||
})
|
||||
|
||||
socket.value.on('added', stream => {
|
||||
const json = JSON.parse(stream);
|
||||
stateStore.add('queue', json.data._id, json.data);
|
||||
toast.success(`Item queued: ${ag(stateStore.get('queue', json.data._id, {}), 'title')}`);
|
||||
});
|
||||
|
||||
['log_info', 'log_success', 'log_warning', 'log_error'].forEach(event => socket.value?.on(event, stream => {
|
||||
const json = JSON.parse(stream);
|
||||
const message = json?.message || json?.data?.message;
|
||||
const data = json.data?.data || json.data || {};
|
||||
switch (event) {
|
||||
case 'log_info':
|
||||
toast.info(message, data);
|
||||
break;
|
||||
case 'log_success':
|
||||
toast.success(message, data);
|
||||
break;
|
||||
case 'log_warning':
|
||||
toast.warning(message, data);
|
||||
break;
|
||||
case 'log_error':
|
||||
toast.error(message, data);
|
||||
break;
|
||||
}
|
||||
}));
|
||||
|
||||
socket.value.on('completed', stream => {
|
||||
const json = JSON.parse(stream);
|
||||
|
||||
if (true === stateStore.has('queue', json.data._id)) {
|
||||
stateStore.remove('queue', json.data._id);
|
||||
}
|
||||
|
||||
stateStore.add('history', json.data._id, json.data);
|
||||
});
|
||||
|
||||
socket.value.on('cancelled', stream => {
|
||||
const item = JSON.parse(stream);
|
||||
const id = item.data._id
|
||||
|
||||
if (true !== stateStore.has('queue', id)) {
|
||||
return
|
||||
}
|
||||
|
||||
toast.warning(`Download cancelled: ${ag(stateStore.get('queue', id, {}), 'title')}`);
|
||||
|
||||
if (true === stateStore.has('queue', id)) {
|
||||
stateStore.remove('queue', id);
|
||||
}
|
||||
});
|
||||
|
||||
socket.value.on('cleared', stream => {
|
||||
const item = JSON.parse(stream);
|
||||
const id = item.data._id
|
||||
|
||||
if (true !== stateStore.has('history', id)) {
|
||||
return
|
||||
}
|
||||
|
||||
stateStore.remove('history', id);
|
||||
});
|
||||
|
||||
socket.value.on("updated", stream => {
|
||||
const json = JSON.parse(stream);
|
||||
const id = json.data._id;
|
||||
|
||||
if (true === stateStore.has('history', id)) {
|
||||
stateStore.update('history', id, json.data);
|
||||
return;
|
||||
}
|
||||
|
||||
stateStore.update('queue', id, json.data);
|
||||
});
|
||||
|
||||
socket.value.on("update", stream => {
|
||||
const json = JSON.parse(stream);
|
||||
if (true === stateStore.has('history', json.data._id)) {
|
||||
stateStore.update('history', json.data._id, json.data);
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
socket.value.on('paused', data => {
|
||||
const json = JSON.parse(data);
|
||||
const pausedState = Boolean(json.data.paused);
|
||||
config.update('paused', pausedState);
|
||||
|
||||
if (false === pausedState) {
|
||||
toast.success('Download queue resumed.');
|
||||
return;
|
||||
}
|
||||
|
||||
toast.warning('Download queue paused.', { timeout: 10000 });
|
||||
});
|
||||
|
||||
socket.value.on('presets_update', (data: string) => config.update('presets', JSON.parse(data).data || []));
|
||||
}
|
||||
|
||||
const emit = (event: string, data?: any): any => socket.value?.emit(event, data)
|
||||
const on = (event: string, callback: (...args: any[]) => void): any => socket.value?.on(event, callback)
|
||||
const off = (event: string, callback: (...args: any[]) => void): any => socket.value?.off(event, callback)
|
||||
|
||||
if (false === isConnected.value) {
|
||||
connect();
|
||||
}
|
||||
|
||||
window.ws = socket.value;
|
||||
|
||||
return { connect, on, off, emit, socket, isConnected };
|
||||
});
|
||||
|
|
@ -1,43 +0,0 @@
|
|||
export const useStateStore = defineStore('state', () => {
|
||||
const state = reactive({
|
||||
queue: {},
|
||||
history: {}
|
||||
});
|
||||
|
||||
const actions = {
|
||||
add(type, key, value) {
|
||||
state[type][key] = value;
|
||||
},
|
||||
update(type, key, value) {
|
||||
state[type][key] = value;
|
||||
},
|
||||
remove(type, key) {
|
||||
if (state[type][key]) {
|
||||
delete state[type][key];
|
||||
}
|
||||
},
|
||||
get(type, key, defaultValue = null) {
|
||||
return state[type][key] || defaultValue;
|
||||
},
|
||||
has(type, key) {
|
||||
return !!state[type][key];
|
||||
},
|
||||
clearAll(type) {
|
||||
state[type] = {};
|
||||
},
|
||||
addAll(type, data) {
|
||||
state[type] = data;
|
||||
},
|
||||
move(fromType, toType, key) {
|
||||
if (state[fromType][key]) {
|
||||
state[toType][key] = state[fromType][key];
|
||||
delete state[fromType][key];
|
||||
}
|
||||
},
|
||||
count(type) {
|
||||
return Object.keys(state[type]).length;
|
||||
}
|
||||
}
|
||||
|
||||
return { ...toRefs(state), ...actions };
|
||||
});
|
||||
59
ui/stores/StateStore.ts
Normal file
59
ui/stores/StateStore.ts
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import { defineStore } from 'pinia'
|
||||
import { reactive, toRefs } from 'vue'
|
||||
import type { StoreItem } from '~/types/store'
|
||||
|
||||
type StateType = 'queue' | 'history'
|
||||
type KeyType = string
|
||||
type ValueType = StoreItem
|
||||
|
||||
interface State {
|
||||
queue: Record<KeyType, ValueType>
|
||||
history: Record<KeyType, ValueType>
|
||||
}
|
||||
|
||||
export const useStateStore = defineStore('state', () => {
|
||||
const state = reactive<State>({ queue: {}, history: {} })
|
||||
|
||||
const add = (type: StateType, key: KeyType, value: ValueType): void => {
|
||||
state[type][key] = value
|
||||
}
|
||||
|
||||
const update = (type: StateType, key: KeyType, value: ValueType): void => {
|
||||
state[type][key] = value
|
||||
}
|
||||
|
||||
const remove = (type: StateType, key: KeyType): void => {
|
||||
if (state[type][key]) {
|
||||
delete state[type][key]
|
||||
}
|
||||
}
|
||||
|
||||
const get = (type: StateType, key: KeyType, defaultValue: ValueType | null | object = null): ValueType | null => {
|
||||
return state[type][key] || defaultValue
|
||||
}
|
||||
|
||||
const has = (type: StateType, key: KeyType): boolean => {
|
||||
return !!state[type][key]
|
||||
}
|
||||
|
||||
const clearAll = (type: StateType): void => {
|
||||
state[type] = {}
|
||||
}
|
||||
|
||||
const addAll = (type: StateType, data: Record<KeyType, ValueType>): void => {
|
||||
state[type] = data
|
||||
}
|
||||
|
||||
const move = (fromType: StateType, toType: StateType, key: KeyType): void => {
|
||||
if (state[fromType][key]) {
|
||||
state[toType][key] = state[fromType][key]
|
||||
delete state[fromType][key]
|
||||
}
|
||||
}
|
||||
|
||||
const count = (type: StateType): number => {
|
||||
return Object.keys(state[type]).length
|
||||
}
|
||||
|
||||
return { ...toRefs(state), add, update, remove, get, has, clearAll, addAll, move, count }
|
||||
})
|
||||
|
|
@ -2,6 +2,9 @@
|
|||
// https://nuxt.com/docs/guide/concepts/typescript
|
||||
"extends": "./.nuxt/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowImportingTsExtensions": false
|
||||
"allowImportingTsExtensions": false,
|
||||
"types": [
|
||||
"./types/globals"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
0
ui/@types/config.d.ts → ui/types/config.d.ts
vendored
0
ui/@types/config.d.ts → ui/types/config.d.ts
vendored
7
ui/types/globals.d.ts
vendored
Normal file
7
ui/types/globals.d.ts
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export { }
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
ws?: unknown
|
||||
}
|
||||
}
|
||||
0
ui/@types/item.d.ts → ui/types/item.d.ts
vendored
0
ui/@types/item.d.ts → ui/types/item.d.ts
vendored
8
ui/types/sockets.d.ts
vendored
Normal file
8
ui/types/sockets.d.ts
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
export type Event = {
|
||||
id: str
|
||||
created_at: str
|
||||
event: str
|
||||
title: str | null = null
|
||||
message: str | null = null
|
||||
data: Any = { }
|
||||
}
|
||||
76
ui/types/store.d.ts
vendored
Normal file
76
ui/types/store.d.ts
vendored
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
export type ItemStatus = 'finished' | 'preparing' | 'error' | 'cancelled' | 'downloading' | 'postprocessing' | 'not_live' | null;
|
||||
export type StoreItem = {
|
||||
/** Unique identifier for the item */
|
||||
_id: string
|
||||
/** Error message if available */
|
||||
error: string | null
|
||||
/** Item source id */
|
||||
id: string
|
||||
/** Title of the item */
|
||||
title: string
|
||||
/** URL of the item */
|
||||
url: string
|
||||
/** Preset used for the item */
|
||||
preset: string
|
||||
/** Folder where the item is saved */
|
||||
folder: string
|
||||
/** Download directory */
|
||||
download_dir: string
|
||||
/** Temporary directory for the item */
|
||||
temp_dir: string
|
||||
/** Status of the item */
|
||||
status: ItemStatus
|
||||
/** If the item has cookies */
|
||||
cookies: string
|
||||
/** If the item has custom output_template */
|
||||
template: string
|
||||
/** If the item has custom output_template for chapters */
|
||||
template_chapter: string
|
||||
/** When the item was created */
|
||||
timestamp: number
|
||||
/** If the item is a live stream */
|
||||
is_live: boolean
|
||||
/** ISO 8601 formatted start time of the item */
|
||||
datetime: string
|
||||
/** Live stream start time if available */
|
||||
live_in: string | null
|
||||
/** File size of the item if available */
|
||||
file_size: number | null
|
||||
/** Custom yt-dlp command line arguments */
|
||||
cli: string
|
||||
/** If the item is auto-started */
|
||||
auto_start: boolean
|
||||
/** Options for the item */
|
||||
options: Record<string, unknown>
|
||||
/** Extras for the item */
|
||||
extras: {
|
||||
/** Which channel the item belongs to */
|
||||
channel?: string
|
||||
/** The video duration if available */
|
||||
duration?: number | null
|
||||
/** The video release date if available */
|
||||
release_in?: string
|
||||
/** The video thumbnail URL if available */
|
||||
thumbnail?: string
|
||||
/** The uploader of the item if available */
|
||||
uploader?: string
|
||||
}
|
||||
/** The item temporary filename */
|
||||
tmpfilename?: string | null
|
||||
/** The item filename */
|
||||
filename?: string | null
|
||||
/** Actual file size of the item if available */
|
||||
total_bytes?: number | null
|
||||
/** Estimated total bytes for the item if available */
|
||||
total_bytes_estimate?: number | null
|
||||
/** Downloaded bytes of the item if available */
|
||||
downloaded_bytes?: number | null
|
||||
/** Message attached with the item if available */
|
||||
msg?: string | null
|
||||
/** Progress percentage of the item if available */
|
||||
percent?: number | null
|
||||
/** Speed of the item download if available */
|
||||
speed?: number | null
|
||||
/** Time remaining for the item download if available */
|
||||
eta?: number | null
|
||||
}
|
||||
|
|
@ -13,7 +13,7 @@ const getValue = (obj) => 'function' === typeof obj ? obj() : obj
|
|||
/**
|
||||
* Get value from object or function and return default value if it's undefined or null
|
||||
*
|
||||
* @param {Object|Array} obj The object to get the value from.
|
||||
* @param {Object|Array|any} obj The object to get the value from.
|
||||
* @param {string} path The path to the value.
|
||||
* @param {*} defaultValue The default value to return if the path is not found.
|
||||
*
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import type { convert_args_response } from "~/@types/responses";
|
||||
import type { convert_args_response } from "~/types/responses";
|
||||
|
||||
const separators = [
|
||||
{ name: 'Comma', value: ',', },
|
||||
|
|
|
|||
Loading…
Reference in a new issue