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
|
from .Singleton import Singleton
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class BackgroundWorker(metaclass=Singleton):
|
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
|
_instance = None
|
||||||
"""The instance of the Notification class."""
|
"""The instance of the Notification class."""
|
||||||
|
|
||||||
|
|
@ -37,19 +43,18 @@ class BackgroundWorker(metaclass=Singleton):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.exception("Loop error: %s", 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:
|
while self.running:
|
||||||
try:
|
try:
|
||||||
fn, args, kwargs = self.queue.get(timeout=1)
|
fn, args, kwargs = self.queue.get(timeout=1)
|
||||||
try:
|
try:
|
||||||
LOG.debug("Running background task: %s", fn.__name__)
|
|
||||||
result = fn(*args, **kwargs)
|
result = fn(*args, **kwargs)
|
||||||
if inspect.iscoroutine(result):
|
if inspect.iscoroutine(result):
|
||||||
loop.call_soon_threadsafe(loop.create_task, result)
|
loop.call_soon_threadsafe(loop.create_task, result)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.exception(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:
|
except Empty:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -492,10 +492,10 @@ class Download:
|
||||||
if "error" == self.info.status and "error" in status:
|
if "error" == self.info.status and "error" in status:
|
||||||
self.info.error = status.get("error")
|
self.info.error = status.get("error")
|
||||||
await self._notify.emit(
|
await self._notify.emit(
|
||||||
Events.ERROR,
|
Events.LOG_ERROR,
|
||||||
data={"message": self.info.error, "data": self.info},
|
data=self.info,
|
||||||
title="Download Error",
|
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:
|
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 .DataStore import DataStore, StoreType
|
||||||
from .Download import Download
|
from .Download import Download
|
||||||
from .Events import EventBus, Events
|
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 .ItemDTO import Item, ItemDTO
|
||||||
from .Presets import Presets
|
from .Presets import Presets
|
||||||
from .Scheduler import Scheduler
|
from .Scheduler import Scheduler
|
||||||
|
|
@ -452,39 +450,48 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
dlInfo: Download = Download(info=dl, info_dict=entry if item.auto_start else None, logs=logs)
|
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 = ""
|
text_logs: str = ""
|
||||||
if filtered_logs := extract_ytdlp_logs(logs):
|
if filtered_logs := extract_ytdlp_logs(logs):
|
||||||
text_logs = f" Logs: {', '.join(filtered_logs)}"
|
text_logs = f" Logs: {', '.join(filtered_logs)}"
|
||||||
|
|
||||||
if "is_upcoming" == entry.get("live_status"):
|
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.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(
|
await self._notify.emit(
|
||||||
Events.LOG_INFO,
|
Events.LOG_INFO,
|
||||||
data=event_info(dlInfo.info.msg, {"lowPriority": True}),
|
data={"lowPriority": True},
|
||||||
title="Premiere video" if is_premiere else "Live Stream",
|
title=notifyTitle,
|
||||||
message=f"Item '{dlInfo.info.title}' is not available yet. {dlInfo.info.msg}",
|
message=notifyMessage,
|
||||||
)
|
)
|
||||||
|
|
||||||
itemDownload: Download = self.done.put(dlInfo)
|
itemDownload: Download = self.done.put(dlInfo)
|
||||||
elif len(entry.get("formats", [])) < 1:
|
elif len(entry.get("formats", [])) < 1:
|
||||||
availability: str = entry.get("availability", "public")
|
ava: str = entry.get("availability", "public")
|
||||||
msg: str = "No formats found."
|
notifyTitle = "Download Error"
|
||||||
if availability and availability not in ("public",):
|
notifyMessage: str = f"No formats for '{dl.title}'."
|
||||||
msg += f" Availability is set for '{availability}'."
|
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"
|
dlInfo.info.status = "error"
|
||||||
itemDownload = self.done.put(dlInfo)
|
itemDownload = self.done.put(dlInfo)
|
||||||
NotifyEvent = Events.COMPLETED
|
notifyEvent = Events.COMPLETED
|
||||||
await self._notify.emit(
|
await self._notify.emit(
|
||||||
Events.LOG_WARNING,
|
Events.LOG_WARNING,
|
||||||
data=event_warning(f"No formats found for '{dl.title}'."),
|
data={"logs": text_logs},
|
||||||
title="No Formats Found",
|
title=notifyTitle,
|
||||||
message=f"No formats found for '{dl.title}'. {dlInfo.info.error}",
|
message=notifyMessage,
|
||||||
)
|
)
|
||||||
elif is_premiere and self.config.prevent_live_premiere:
|
elif is_premiere and self.config.prevent_live_premiere:
|
||||||
|
notifyTitle = "Premiere Video"
|
||||||
dlInfo.info.error = "Premiering right now."
|
dlInfo.info.error = "Premiering right now."
|
||||||
|
|
||||||
_requeue = True
|
_requeue = True
|
||||||
|
|
@ -496,6 +503,7 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
)
|
)
|
||||||
starts_in = starts_in + timedelta(minutes=5, seconds=dl.extras.get("duration", 0))
|
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()}."
|
dlInfo.info.error += f" Download will start at {starts_in.astimezone().isoformat()}."
|
||||||
|
notifyMessage = dlInfo.info.error.strip()
|
||||||
_requeue = False
|
_requeue = False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(f"Failed to parse live_in date '{release_in}'. {e!s}")
|
LOG.error(f"Failed to parse live_in date '{release_in}'. {e!s}")
|
||||||
|
|
@ -503,25 +511,24 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
else:
|
else:
|
||||||
dlInfo.info.error += f" Delaying download by '{300+dl.extras.get('duration',0)}' seconds."
|
dlInfo.info.error += f" Delaying download by '{300+dl.extras.get('duration',0)}' seconds."
|
||||||
|
|
||||||
|
notifyMessage = dlInfo.info.error.strip()
|
||||||
|
|
||||||
if _requeue:
|
if _requeue:
|
||||||
NotifyEvent = Events.ADDED
|
notifyEvent = Events.ADDED
|
||||||
itemDownload = self.queue.put(dlInfo)
|
itemDownload = self.queue.put(dlInfo)
|
||||||
if item.auto_start:
|
if item.auto_start:
|
||||||
self.event.set()
|
self.event.set()
|
||||||
else:
|
|
||||||
LOG.debug(f"Item {itemDownload.info.name()} is not set to auto-start.")
|
|
||||||
else:
|
else:
|
||||||
dlInfo.info.status = "not_live"
|
dlInfo.info.status = "not_live"
|
||||||
itemDownload = self.done.put(dlInfo)
|
itemDownload = self.done.put(dlInfo)
|
||||||
NotifyEvent = Events.COMPLETED
|
notifyEvent = Events.COMPLETED
|
||||||
await self._notify.emit(
|
notifyTitle = "Item Not Live"
|
||||||
Events.LOG_INFO,
|
notifyMessage = f"Item '{dlInfo.info.title}' is not live."
|
||||||
data=event_info(f"'{dl.title}' is {dlInfo.info.error}.", {"lowPriority": True}),
|
await self._notify.emit(Events.LOG_INFO, title=notifyTitle, message=notifyMessage)
|
||||||
title="Item Not Live",
|
|
||||||
message=f"Item '{dl.title}' is not live. {dlInfo.info.error}",
|
|
||||||
)
|
|
||||||
else:
|
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)
|
itemDownload = self.queue.put(dlInfo)
|
||||||
if item.auto_start:
|
if item.auto_start:
|
||||||
self.event.set()
|
self.event.set()
|
||||||
|
|
@ -529,10 +536,10 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
LOG.debug(f"Item {itemDownload.info.name()} is not set to auto-start.")
|
LOG.debug(f"Item {itemDownload.info.name()} is not set to auto-start.")
|
||||||
|
|
||||||
await self._notify.emit(
|
await self._notify.emit(
|
||||||
NotifyEvent,
|
notifyEvent,
|
||||||
data=itemDownload.info.serialize(),
|
data=itemDownload.info.serialize(),
|
||||||
title="Item Added" if Events.ADDED == NotifyEvent else None,
|
title=notifyTitle,
|
||||||
message=f"Added '{itemDownload.info.title}'." if NotifyEvent == Events.ADDED else None,
|
message=notifyMessage,
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
@ -740,14 +747,18 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
self.queue.delete(id)
|
self.queue.delete(id)
|
||||||
await self._notify.emit(
|
await self._notify.emit(
|
||||||
Events.CANCELLED,
|
Events.CANCELLED,
|
||||||
data=item.info.serialize(),
|
data=item.info,
|
||||||
title="Download Cancelled",
|
title="Download Cancelled",
|
||||||
message=f"Download '{item.info.title}' has been cancelled.",
|
message=f"Download '{item.info.title}' has been cancelled.",
|
||||||
)
|
)
|
||||||
item.info.status = "cancelled"
|
item.info.status = "cancelled"
|
||||||
# item.info.error = "Cancelled by user."
|
|
||||||
self.done.put(item)
|
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}")
|
LOG.info(f"Deleted from queue {item_ref}")
|
||||||
|
|
||||||
status[id] = "ok"
|
status[id] = "ok"
|
||||||
|
|
@ -815,11 +826,13 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
LOG.error(f"Unable to remove '{itemRef}' local file '{filename}'. {e!s}")
|
LOG.error(f"Unable to remove '{itemRef}' local file '{filename}'. {e!s}")
|
||||||
|
|
||||||
self.done.delete(id)
|
self.done.delete(id)
|
||||||
|
|
||||||
|
_status: str = "Removed" if removed_files > 0 else "Cleared"
|
||||||
await self._notify.emit(
|
await self._notify.emit(
|
||||||
Events.CLEARED,
|
Events.CLEARED,
|
||||||
data=item.info.serialize(),
|
data=item.info,
|
||||||
title="Download Cleared",
|
title=f"Download {_status}",
|
||||||
message=f"Cleared download '{item.info.title}' from history.",
|
message=f"{_status} '{item.info.title}' from history.",
|
||||||
)
|
)
|
||||||
|
|
||||||
msg = f"Deleted completed download '{itemRef}'."
|
msg = f"Deleted completed download '{itemRef}'."
|
||||||
|
|
@ -857,6 +870,27 @@ class DownloadQueue(metaclass=Singleton):
|
||||||
|
|
||||||
return items
|
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:
|
async def _download_pool(self) -> None:
|
||||||
"""
|
"""
|
||||||
Create a pool of workers to download the files.
|
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.")
|
LOG.debug(f"Download Task '{id}' is completed. Removing from queue.")
|
||||||
self.queue.delete(key=id)
|
self.queue.delete(key=id)
|
||||||
|
|
||||||
|
notifyTitle: str | None = None
|
||||||
|
notifyMessage: str | None = None
|
||||||
|
|
||||||
if entry.is_cancelled() is True:
|
if entry.is_cancelled() is True:
|
||||||
await self._notify.emit(
|
await self._notify.emit(
|
||||||
Events.CANCELLED,
|
Events.CANCELLED,
|
||||||
data=entry.info.serialize(),
|
data=entry.info,
|
||||||
title="Download Cancelled",
|
title="Download Cancelled",
|
||||||
message=f"Download '{entry.info.title}' has been cancelled.",
|
message=f"Download '{entry.info.title}' has been cancelled.",
|
||||||
)
|
)
|
||||||
entry.info.status = "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)
|
self.done.put(value=entry)
|
||||||
await self._notify.emit(
|
await self._notify.emit(Events.COMPLETED, data=entry.info, title=notifyTitle, message=notifyMessage)
|
||||||
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,
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
LOG.warning(f"Download '{id}' not found in queue.")
|
LOG.warning(f"Download '{id}' not found in queue.")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,85 +6,10 @@ from collections.abc import Awaitable
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
from .BackgroundWorker import BackgroundWorker
|
||||||
from .Singleton import Singleton
|
from .Singleton import Singleton
|
||||||
|
|
||||||
LOG = logging.getLogger("Events")
|
LOG: logging.Logger = 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 {}}
|
|
||||||
|
|
||||||
|
|
||||||
class Events:
|
class Events:
|
||||||
|
|
@ -98,30 +23,32 @@ class Events:
|
||||||
SHUTDOWN = "shutdown"
|
SHUTDOWN = "shutdown"
|
||||||
|
|
||||||
ADDED = "added"
|
ADDED = "added"
|
||||||
|
UPDATE = "update"
|
||||||
UPDATED = "updated"
|
UPDATED = "updated"
|
||||||
COMPLETED = "completed"
|
COMPLETED = "completed"
|
||||||
CANCELLED = "cancelled"
|
CANCELLED = "cancelled"
|
||||||
CLEARED = "cleared"
|
CLEARED = "cleared"
|
||||||
ERROR = "error"
|
CONNECTED = "connected"
|
||||||
|
STATUS = "status"
|
||||||
|
|
||||||
LOG_INFO = "log_info"
|
LOG_INFO = "log_info"
|
||||||
LOG_WARNING = "log_warning"
|
LOG_WARNING = "log_warning"
|
||||||
LOG_ERROR = "log_error"
|
LOG_ERROR = "log_error"
|
||||||
LOG_SUCCESS = "log_success"
|
LOG_SUCCESS = "log_success"
|
||||||
|
|
||||||
INITIAL_DATA = "initial_data"
|
|
||||||
ITEM_DELETE = "item_delete"
|
ITEM_DELETE = "item_delete"
|
||||||
ITEM_CANCEL = "item_cancel"
|
ITEM_CANCEL = "item_cancel"
|
||||||
ITEM_ERROR = "item_error"
|
ITEM_ERROR = "item_error"
|
||||||
STATUS = "status"
|
|
||||||
CLI_CLOSE = "cli_close"
|
|
||||||
CLI_OUTPUT = "cli_output"
|
|
||||||
UPDATE = "update"
|
|
||||||
TEST = "test"
|
TEST = "test"
|
||||||
ADD_URL = "add_url"
|
ADD_URL = "add_url"
|
||||||
|
|
||||||
CLI_POST = "cli_post"
|
|
||||||
PAUSED = "paused"
|
PAUSED = "paused"
|
||||||
|
|
||||||
|
CLI_POST = "cli_post"
|
||||||
|
CLI_CLOSE = "cli_close"
|
||||||
|
CLI_OUTPUT = "cli_output"
|
||||||
|
|
||||||
TASKS_ADD = "task_add"
|
TASKS_ADD = "task_add"
|
||||||
TASK_DISPATCHED = "task_dispatched"
|
TASK_DISPATCHED = "task_dispatched"
|
||||||
TASK_FINISHED = "task_finished"
|
TASK_FINISHED = "task_finished"
|
||||||
|
|
@ -129,6 +56,7 @@ class Events:
|
||||||
|
|
||||||
PRESETS_ADD = "presets_add"
|
PRESETS_ADD = "presets_add"
|
||||||
PRESETS_UPDATE = "presets_update"
|
PRESETS_UPDATE = "presets_update"
|
||||||
|
|
||||||
SCHEDULE_ADD = "schedule_add"
|
SCHEDULE_ADD = "schedule_add"
|
||||||
|
|
||||||
CONDITIONS_ADD = "conditions_add"
|
CONDITIONS_ADD = "conditions_add"
|
||||||
|
|
@ -158,9 +86,8 @@ class Events:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return [
|
return [
|
||||||
Events.INITIAL_DATA,
|
Events.CONNECTED,
|
||||||
Events.ADDED,
|
Events.ADDED,
|
||||||
Events.ERROR,
|
|
||||||
Events.LOG_INFO,
|
Events.LOG_INFO,
|
||||||
Events.LOG_WARNING,
|
Events.LOG_WARNING,
|
||||||
Events.LOG_ERROR,
|
Events.LOG_ERROR,
|
||||||
|
|
@ -185,7 +112,7 @@ class Events:
|
||||||
list: The list of debug events.
|
list: The list of debug events.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return [Events.UPDATED]
|
return [Events.UPDATED, Events.CLI_OUTPUT]
|
||||||
|
|
||||||
|
|
||||||
@dataclass(kw_only=True)
|
@dataclass(kw_only=True)
|
||||||
|
|
@ -209,7 +136,7 @@ class Event:
|
||||||
message: str | None = None
|
message: str | None = None
|
||||||
"""The message of the event, if any."""
|
"""The message of the event, if any."""
|
||||||
|
|
||||||
data: any
|
data: Any
|
||||||
"""The data that was passed to the event."""
|
"""The data that was passed to the event."""
|
||||||
|
|
||||||
def serialize(self) -> dict:
|
def serialize(self) -> dict:
|
||||||
|
|
@ -277,12 +204,16 @@ class EventBus(metaclass=Singleton):
|
||||||
debug: bool = False
|
debug: bool = False
|
||||||
"""Whether to log debug messages or not."""
|
"""Whether to log debug messages or not."""
|
||||||
|
|
||||||
|
_offload: BackgroundWorker
|
||||||
|
"""The background worker to offload tasks to."""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
EventBus._instance = self
|
EventBus._instance = self
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
|
|
||||||
self.debug = Config.get_instance().debug
|
self.debug = Config.get_instance().debug
|
||||||
|
self._offload = BackgroundWorker.get_instance()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_instance() -> "EventBus":
|
def get_instance() -> "EventBus":
|
||||||
|
|
@ -368,13 +299,24 @@ class EventBus(metaclass=Singleton):
|
||||||
|
|
||||||
return self
|
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.
|
Emit event and (optionally) wait for results.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
event (str): The event to emit.
|
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.
|
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.
|
wait (bool): Whether to wait for the results of the event handlers. Defaults to True.
|
||||||
**kwargs: Additional keyword arguments to pass to the event
|
**kwargs: Additional keyword arguments to pass to the event
|
||||||
|
|
@ -389,8 +331,11 @@ class EventBus(metaclass=Singleton):
|
||||||
if event not in self._listeners:
|
if event not in self._listeners:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
if not data:
|
||||||
|
data = {}
|
||||||
|
|
||||||
async def emit_all():
|
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})
|
LOG.debug(f"Emitting event '{ev.id}: {ev.event}'.", extra={"data": data})
|
||||||
|
|
||||||
res: list = []
|
res: list = []
|
||||||
|
|
@ -428,14 +373,14 @@ class EventBus(metaclass=Singleton):
|
||||||
return fut.result() if wait else fut
|
return fut.result() if wait else fut
|
||||||
|
|
||||||
async def emit(
|
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:
|
) -> Awaitable:
|
||||||
"""
|
"""
|
||||||
Emit an event.
|
Emit an event.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
event (str): The event to emit.
|
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.
|
title (str | None): The title of the event, if any.
|
||||||
message (str | None): The message of the event, if any.
|
message (str | None): The message of the event, if any.
|
||||||
**kwargs: The keyword arguments to pass to the event.
|
**kwargs: The keyword arguments to pass to the event.
|
||||||
|
|
@ -447,7 +392,10 @@ class EventBus(metaclass=Singleton):
|
||||||
if event not in self._listeners:
|
if event not in self._listeners:
|
||||||
return []
|
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():
|
if self.debug or event not in Events.only_debug():
|
||||||
LOG.debug(f"Emitting event '{ev.id}: {ev.event}'.", extra={"data": data})
|
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}'.")
|
LOG.error(f"Failed to emit event '{ev.event}' to '{handler.name}'. Error message '{e!s}'.")
|
||||||
|
|
||||||
return asyncio.gather(*tasks)
|
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
|
self.rootPath = root_path
|
||||||
|
|
||||||
def emit(e: Event, _, **kwargs):
|
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 = Services.get_instance()
|
||||||
services.add_all(
|
services.add_all(
|
||||||
|
|
|
||||||
|
|
@ -92,7 +92,6 @@ class Target:
|
||||||
class NotificationEvents:
|
class NotificationEvents:
|
||||||
ADDED = Events.ADDED
|
ADDED = Events.ADDED
|
||||||
COMPLETED = Events.COMPLETED
|
COMPLETED = Events.COMPLETED
|
||||||
ERROR = Events.ERROR
|
|
||||||
CANCELLED = Events.CANCELLED
|
CANCELLED = Events.CANCELLED
|
||||||
CLEARED = Events.CLEARED
|
CLEARED = Events.CLEARED
|
||||||
LOG_INFO = Events.LOG_INFO
|
LOG_INFO = Events.LOG_INFO
|
||||||
|
|
@ -308,10 +307,17 @@ class Notification(metaclass=Singleton):
|
||||||
msg = "Invalid notification target. Invalid 'on' event list found."
|
msg = "Invalid notification target. Invalid 'on' event list found."
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
|
removed_events = []
|
||||||
|
all_events = NotificationEvents.get_events().values()
|
||||||
for e in target["on"]:
|
for e in target["on"]:
|
||||||
if e not in NotificationEvents.get_events().values():
|
if e not in all_events:
|
||||||
msg = f"Invalid notification target. Invalid event '{e}' found."
|
removed_events.append(e)
|
||||||
raise ValueError(msg)
|
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 "headers" in target["request"]:
|
||||||
if not isinstance(target["request"]["headers"], list):
|
if not isinstance(target["request"]["headers"], list):
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,7 @@ from app.library.Services import Services
|
||||||
|
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .encoder import Encoder
|
from .encoder import Encoder
|
||||||
from .Events import EventBus, Events, error, success
|
from .Events import EventBus, Events
|
||||||
from .Scheduler import Scheduler
|
from .Scheduler import Scheduler
|
||||||
from .Singleton import Singleton
|
from .Singleton import Singleton
|
||||||
from .Utils import init_class, validate_url
|
from .Utils import init_class, validate_url
|
||||||
|
|
@ -325,7 +325,7 @@ class Tasks(metaclass=Singleton):
|
||||||
"template": template,
|
"template": template,
|
||||||
"cli": cli,
|
"cli": cli,
|
||||||
},
|
},
|
||||||
title=f"Task '{task.name}' started",
|
title="Tasks",
|
||||||
message=f"Task '{task.name}' started at '{timeNow}'",
|
message=f"Task '{task.name}' started at '{timeNow}'",
|
||||||
id=task.id,
|
id=task.id,
|
||||||
)
|
)
|
||||||
|
|
@ -337,19 +337,16 @@ class Tasks(metaclass=Singleton):
|
||||||
|
|
||||||
await self._notify.emit(
|
await self._notify.emit(
|
||||||
Events.LOG_SUCCESS,
|
Events.LOG_SUCCESS,
|
||||||
data=success(
|
data={"lowPriority": True},
|
||||||
f"Task '{task.name}' completed in '{ended - started:.2f}' seconds.", data={"lowPriority": True}
|
title="Task completed",
|
||||||
),
|
message=f"Task '{task.name}' completed in '{ended - started:.2f}'.",
|
||||||
title=f"Task '{task.name}' completed",
|
|
||||||
message=f"Task '{task.name}' completed at '{timeNow}'",
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(f"Failed to execute '{task.name}' at '{timeNow}'. '{e!s}'.")
|
LOG.error(f"Failed to execute '{task.name}' at '{timeNow}'. '{e!s}'.")
|
||||||
await self._notify.emit(
|
await self._notify.emit(
|
||||||
Events.ERROR,
|
Events.LOG_ERROR,
|
||||||
data=error(f"Failed to execute '{task.name}' at '{timeNow}'. '{e!s}'."),
|
title="Task failed",
|
||||||
title=f"Task '{task.name}' failed",
|
message=f"Failed to execute '{task.name}'. '{e!s}'",
|
||||||
message=str(e),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
23
app/main.py
23
app/main.py
|
|
@ -111,7 +111,12 @@ class Main:
|
||||||
host = host or self._config.host
|
host = host or self._config.host
|
||||||
port = port or self._config.port
|
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)
|
Scheduler.get_instance().attach(self._app)
|
||||||
|
|
||||||
self._socket.attach(self._app)
|
self._socket.attach(self._app)
|
||||||
|
|
@ -123,7 +128,12 @@ class Main:
|
||||||
Notification.get_instance().attach(self._app)
|
Notification.get_instance().attach(self._app)
|
||||||
Conditions.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(_):
|
def started(_):
|
||||||
LOG.info("=" * 40)
|
LOG.info("=" * 40)
|
||||||
|
|
@ -132,7 +142,14 @@ class Main:
|
||||||
|
|
||||||
loop = asyncio.get_event_loop()
|
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:
|
if loop and self._config.debug:
|
||||||
loop.set_debug(True)
|
loop.set_debug(True)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ from aiohttp import web
|
||||||
from aiohttp.web import Request, Response
|
from aiohttp.web import Request, Response
|
||||||
|
|
||||||
from app.library.encoder import Encoder
|
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.Notifications import Notification, NotificationEvents
|
||||||
from app.library.router import route
|
from app.library.router import route
|
||||||
from app.library.Utils import validate_uuid
|
from app.library.Utils import validate_uuid
|
||||||
|
|
@ -104,8 +104,6 @@ async def notification_test(encoder: Encoder, notify: EventBus) -> Response:
|
||||||
Response: The response object.
|
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={}, status=web.HTTPOk.status_code, dumps=encoder.encode)
|
||||||
|
|
||||||
return web.json_response(data=data, status=web.HTTPOk.status_code, dumps=encoder.encode)
|
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,13 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import socketio
|
import socketio
|
||||||
|
|
||||||
from app.library.config import Config
|
from app.library.config import Config
|
||||||
from app.library.DownloadQueue import DownloadQueue
|
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.Presets import Presets
|
||||||
from app.library.router import RouteType, route
|
from app.library.router import RouteType, route
|
||||||
from app.library.Utils import tail_log
|
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()]
|
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")
|
@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")
|
@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.
|
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.
|
data (str): The event to subscribe to.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if not isinstance(data, str):
|
if not isinstance(data, str) or not data:
|
||||||
await notify.emit(Events.ERROR, data=error("Invalid event."), to=sid)
|
await notify.emit(
|
||||||
|
Events.LOG_ERROR,
|
||||||
|
title="Subscription Error",
|
||||||
|
message="Invalid event type was expecting a string.",
|
||||||
|
to=sid,
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
if data not in _Data.subscribers:
|
if data not in _Data.subscribers:
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ import anyio
|
||||||
|
|
||||||
from app.library.config import Config
|
from app.library.config import Config
|
||||||
from app.library.DownloadQueue import DownloadQueue
|
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.ItemDTO import Item
|
||||||
from app.library.router import RouteType, route
|
from app.library.router import RouteType, route
|
||||||
from app.library.Utils import is_downloaded
|
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")
|
url: str | None = data.get("url")
|
||||||
|
|
||||||
if not 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
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
status = await queue.add(item=Item.format(data))
|
||||||
await notify.emit(
|
await notify.emit(
|
||||||
event=Events.STATUS,
|
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,
|
to=sid,
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
LOG.exception(e)
|
LOG.exception(e)
|
||||||
await notify.emit(Events.ERROR, data=error(str(e)), to=sid)
|
await notify.emit(Events.LOG_ERROR, title="Error Adding URL", message=str(e), to=sid)
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
@route(RouteType.SOCKET, "item_cancel", "item_cancel")
|
@route(RouteType.SOCKET, "item_cancel", "item_cancel")
|
||||||
async def item_cancel(queue: DownloadQueue, notify: EventBus, sid: str, data: str):
|
async def item_cancel(queue: DownloadQueue, notify: EventBus, sid: str, data: str):
|
||||||
if not data:
|
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
|
return
|
||||||
|
|
||||||
status: dict[str, str] = {}
|
status: dict[str, str] = {}
|
||||||
status = await queue.cancel([data])
|
status = await queue.cancel([data])
|
||||||
status.update({"identifier": 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")
|
@route(RouteType.SOCKET, "item_delete", "item_delete")
|
||||||
async def item_delete(queue: DownloadQueue, notify: EventBus, sid: str, data: dict):
|
async def item_delete(queue: DownloadQueue, notify: EventBus, sid: str, data: dict):
|
||||||
if not data:
|
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
|
return
|
||||||
|
|
||||||
id: str | None = data.get("id")
|
id: str | None = data.get("id")
|
||||||
if not 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
|
return
|
||||||
|
|
||||||
status: dict[str, str] = {}
|
status: dict[str, str] = {}
|
||||||
|
|
@ -87,7 +121,7 @@ async def item_delete(queue: DownloadQueue, notify: EventBus, sid: str, data: di
|
||||||
status.update({"identifier": id})
|
status.update({"identifier": id})
|
||||||
|
|
||||||
await notify.emit(
|
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")
|
@route(RouteType.SOCKET, "item_start", "item_start")
|
||||||
async def item_start(queue: DownloadQueue, notify: EventBus, sid: str, data: list | str) -> None:
|
async def item_start(queue: DownloadQueue, notify: EventBus, sid: str, data: list | str) -> None:
|
||||||
if not data:
|
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
|
return
|
||||||
|
|
||||||
if isinstance(data, str):
|
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")
|
@route(RouteType.SOCKET, "item_pause", "item_pause")
|
||||||
async def item_pause(queue: DownloadQueue, notify: EventBus, sid: str, data: list | str) -> None:
|
async def item_pause(queue: DownloadQueue, notify: EventBus, sid: str, data: list | str) -> None:
|
||||||
if not data:
|
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
|
return
|
||||||
|
|
||||||
if isinstance(data, str):
|
if isinstance(data, str):
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import os
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
from app.library.config import Config
|
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
|
from app.library.router import RouteType, route
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
|
|
@ -17,7 +17,12 @@ LOG: logging.Logger = logging.getLogger(__name__)
|
||||||
@route(RouteType.SOCKET, "cli_post", "socket_cli_post")
|
@route(RouteType.SOCKET, "cli_post", "socket_cli_post")
|
||||||
async def cli_post(config: Config, notify: EventBus, sid: str, data: str):
|
async def cli_post(config: Config, notify: EventBus, sid: str, data: str):
|
||||||
if not config.console_enabled:
|
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
|
return
|
||||||
|
|
||||||
if not data:
|
if not data:
|
||||||
|
|
@ -29,6 +34,7 @@ async def cli_post(config: Config, notify: EventBus, sid: str, data: str):
|
||||||
import shlex
|
import shlex
|
||||||
import subprocess # ignore
|
import subprocess # ignore
|
||||||
|
|
||||||
|
returncode: int = -1
|
||||||
try:
|
try:
|
||||||
LOG.info(f"Cli command from client '{sid}'. '{data}'")
|
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: dict[str, str] = os.environ.copy()
|
||||||
_env.update(
|
_env.update(
|
||||||
{
|
{
|
||||||
"TERM": "xterm-256color",
|
|
||||||
"LANG": "en_US.UTF-8",
|
|
||||||
"SHELL": "/bin/bash",
|
|
||||||
"LC_ALL": "en_US.UTF-8",
|
|
||||||
"PWD": config.download_path,
|
"PWD": config.download_path,
|
||||||
"FORCE_COLOR": "1",
|
"FORCE_COLOR": "1",
|
||||||
"PYTHONUNBUFFERED": "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:
|
try:
|
||||||
import pty
|
import pty
|
||||||
|
|
||||||
|
|
@ -126,10 +138,9 @@ async def cli_post(config: Config, notify: EventBus, sid: str, data: str):
|
||||||
returncode: int = await proc.wait()
|
returncode: int = await proc.wait()
|
||||||
|
|
||||||
await read_task
|
await read_task
|
||||||
|
|
||||||
await notify.emit(Events.CLI_CLOSE, data={"exitcode": returncode}, to=sid)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LOG.error(f"CLI execute exception was thrown for client '{sid}'.")
|
LOG.error(f"CLI execute exception was thrown for client '{sid}'.")
|
||||||
LOG.exception(e)
|
LOG.exception(e)
|
||||||
await notify.emit(Events.CLI_OUTPUT, data={"type": "stderr", "line": str(e)}, to=sid)
|
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-spin-10 {
|
||||||
--fa-animation-iteration-count: 10;
|
--fa-animation-iteration-count: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.Vue-Toastification__toast-body {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -209,7 +209,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import { decode } from '~/utils/importer'
|
import { decode } from '~/utils/importer'
|
||||||
import type { ConditionItem, ImportedConditionItem } from '~/@types/conditions'
|
import type { ConditionItem, ImportedConditionItem } from '~/types/conditions'
|
||||||
|
|
||||||
const emitter = defineEmits<{
|
const emitter = defineEmits<{
|
||||||
(e: 'cancel'): void
|
(e: 'cancel'): void
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@
|
||||||
<footer class="modal-card-foot p-5">
|
<footer class="modal-card-foot p-5">
|
||||||
<div class="field is-grouped" style="width:100%">
|
<div class="field is-grouped" style="width:100%">
|
||||||
<div class="control is-expanded">
|
<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 }}
|
{{ confirm_button_label }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -83,21 +83,25 @@ const emit = defineEmits<{
|
||||||
(e: 'cancel'): void
|
(e: 'cancel'): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
|
const confirmBtn = ref<HTMLButtonElement | null>(null)
|
||||||
const selected = reactive<Record<string, boolean>>({})
|
const selected = reactive<Record<string, boolean>>({})
|
||||||
|
|
||||||
watch(() => props.visible, visible => {
|
watch(() => props.visible, async visible => {
|
||||||
if (visible && props.options) {
|
if (!visible) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.options) {
|
||||||
for (const opt of props.options) {
|
for (const opt of props.options) {
|
||||||
selected[opt.key] ??= false
|
selected[opt.key] ??= false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
function handleConfirm() {
|
await nextTick()
|
||||||
emit('confirm', { ...selected })
|
confirmBtn.value?.focus()
|
||||||
}
|
}, { immediate: true })
|
||||||
|
|
||||||
function cancel() {
|
|
||||||
emit('cancel')
|
const handleConfirm = () => emit('confirm', { ...selected })
|
||||||
}
|
const cancel = () => emit('cancel')
|
||||||
</script>
|
</script>
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import 'assets/css/bulma-switch.css'
|
import 'assets/css/bulma-switch.css'
|
||||||
import { useStorage } from '@vueuse/core'
|
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'
|
import { getSeparatorsName, separators } from '~/utils/utils'
|
||||||
|
|
||||||
const props = defineProps<{ item?: Partial<item_request> }>()
|
const props = defineProps<{ item?: Partial<item_request> }>()
|
||||||
|
|
|
||||||
|
|
@ -72,7 +72,7 @@ hr {
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import moment from 'moment'
|
import moment from 'moment'
|
||||||
import type { changelogs, changeset } from '~/@types/changelogs'
|
import type { changelogs, changeset } from '~/types/changelogs'
|
||||||
|
|
||||||
const toast = useNotification()
|
const toast = useNotification()
|
||||||
const config = useConfigStore()
|
const config = useConfigStore()
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { request } from '~/utils/index'
|
import { request } from '~/utils/index'
|
||||||
import { encode } from '~/utils/importer'
|
import { encode } from '~/utils/importer'
|
||||||
import type { ConditionItem, ImportedConditionItem } from '~/@types/conditions'
|
import type { ConditionItem, ImportedConditionItem } from '~/types/conditions'
|
||||||
|
|
||||||
const toast = useNotification()
|
const toast = useNotification()
|
||||||
const config = useConfigStore()
|
const config = useConfigStore()
|
||||||
|
|
|
||||||
|
|
@ -63,6 +63,7 @@ import { useStorage } from '@vueuse/core'
|
||||||
|
|
||||||
const config = useConfigStore()
|
const config = useConfigStore()
|
||||||
const socket = useSocketStore()
|
const socket = useSocketStore()
|
||||||
|
const toast = useNotification()
|
||||||
|
|
||||||
const bg_enable = useStorage<boolean>('random_bg', true)
|
const bg_enable = useStorage<boolean>('random_bg', true)
|
||||||
const bg_opacity = useStorage<number>('random_bg_opacity', 0.95)
|
const bg_opacity = useStorage<number>('random_bg_opacity', 0.95)
|
||||||
|
|
@ -88,14 +89,15 @@ watch(() => config.app.basic_mode, async () => {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
await navigateTo('/')
|
await navigateTo('/')
|
||||||
})
|
}, { immediate: true })
|
||||||
|
|
||||||
watch(() => config.app.console_enabled, async () => {
|
watch(() => config.app.console_enabled, async () => {
|
||||||
if (config.app.console_enabled) {
|
if (config.app.console_enabled) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
toast.error('Console is disabled in the configuration. Please enable it to use this feature.')
|
||||||
await navigateTo('/')
|
await navigateTo('/')
|
||||||
})
|
}, { immediate: true })
|
||||||
|
|
||||||
const handle_event = () => {
|
const handle_event = () => {
|
||||||
if (!terminal.value) {
|
if (!terminal.value) {
|
||||||
|
|
@ -171,9 +173,9 @@ const writer = (s: string) => {
|
||||||
return
|
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
|
const loader = () => isLoading.value = false
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import type { item_request } from '~/@types/item'
|
import type { item_request } from '~/types/item'
|
||||||
|
|
||||||
const config = useConfigStore()
|
const config = useConfigStore()
|
||||||
const stateStore = useStateStore()
|
const stateStore = useStateStore()
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ import moment from 'moment'
|
||||||
import { request } from '~/utils/index'
|
import { request } from '~/utils/index'
|
||||||
import { ref, onMounted, nextTick } from 'vue'
|
import { ref, onMounted, nextTick } from 'vue'
|
||||||
import { useStorage } from '@vueuse/core'
|
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
|
let scrollTimeout: NodeJS.Timeout | null = null
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -330,7 +330,7 @@ import { useStorage } from '@vueuse/core'
|
||||||
import { CronExpressionParser } from 'cron-parser'
|
import { CronExpressionParser } from 'cron-parser'
|
||||||
import { request, sleep } from '~/utils/index'
|
import { request, sleep } from '~/utils/index'
|
||||||
import { encode } from '~/utils/importer'
|
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 box = useConfirm()
|
||||||
const toast = useNotification()
|
const toast = useNotification()
|
||||||
|
|
@ -686,7 +686,8 @@ const runNow = async (item: task_item, mass: boolean = false) => {
|
||||||
onBeforeUnmount(() => socket.off('status', statusHandler))
|
onBeforeUnmount(() => socket.off('status', statusHandler))
|
||||||
|
|
||||||
const statusHandler = async (stream: string) => {
|
const statusHandler = async (stream: string) => {
|
||||||
const { status, msg } = JSON.parse(stream)
|
const json = JSON.parse(stream)
|
||||||
|
const { status, msg } = json.data
|
||||||
|
|
||||||
if ('error' === status) {
|
if ('error' === status) {
|
||||||
toast.error(msg)
|
toast.error(msg)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { useStorage } from '@vueuse/core'
|
import { useStorage } from '@vueuse/core'
|
||||||
import type { ConfigState } from '~/@types/config';
|
import type { ConfigState } from '~/types/config';
|
||||||
|
|
||||||
export const useConfigStore = defineStore('config', () => {
|
export const useConfigStore = defineStore('config', () => {
|
||||||
const state = reactive<ConfigState>({
|
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
|
// https://nuxt.com/docs/guide/concepts/typescript
|
||||||
"extends": "./.nuxt/tsconfig.json",
|
"extends": "./.nuxt/tsconfig.json",
|
||||||
"compilerOptions": {
|
"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
|
* 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 {string} path The path to the value.
|
||||||
* @param {*} defaultValue The default value to return if the path is not found.
|
* @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 = [
|
const separators = [
|
||||||
{ name: 'Comma', value: ',', },
|
{ name: 'Comma', value: ',', },
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue