Completed the events system upgrade and expanded the notifications events with support for apprise.

This commit is contained in:
arabcoders 2025-07-19 19:45:07 +03:00
parent 0f7f22bdbf
commit a7e8d242c9
19 changed files with 1639 additions and 1110 deletions

View file

@ -14,7 +14,7 @@ live streams, and includes features like scheduling downloads, sending notificat
* Random beautiful background. `can be disabled or source changed`.
* Can handle live streams.
* Scheduler to queue channels or playlists to be downloaded automatically at a specified time.
* Send notification to targets based on selected events.
* Send notification to targets based on selected events. includes [Apprise](https://github.com/caronc/apprise?tab=readme-ov-file#readme) support for non http/https URLs.
* Support per link `cli options` & `cookies`.
* Queue multiple URLs separated by comma.
* Presets system to re-use frequently used yt-dlp options.
@ -31,13 +31,7 @@ live streams, and includes features like scheduling downloads, sending notificat
* Automatic upcoming live stream re-queue.
* Apply `yt-dlp` options per custom defined conditions.
* Custom browser extensions, bookmarklets and iOS shortcuts to send links to YTPTube instance.
### Side project
We have a side project related to YTPTube, which takes YTPTube source code and build an executable for Windows, macOS and Linux.
The project is in early stages, and might not even work yet. However keep an eye at [This build action](https://github.com/arabcoders/ytptube/actions/workflows/native-build.yml) for builds, simply select last successful build and download the executable for your platform. Help us test out the platforms
and report any issues you might find. We only have windows/linux machines to test on, so we need your help to test it out on macOS.
* A executable for Windows, macOS and Linux, which can be found in the release page.
# Installation

View file

@ -4,9 +4,15 @@ import logging
import threading
from queue import Empty, Queue
from aiohttp import web
from .Singleton import Singleton
LOG: logging.Logger = logging.getLogger(__name__)
LOG: logging.Logger = logging.getLogger("BackgroundWorker")
class CloseThread:
pass
class BackgroundWorker(metaclass=Singleton):
@ -19,12 +25,30 @@ class BackgroundWorker(metaclass=Singleton):
_instance = None
"""The instance of the Notification class."""
thread: threading.Thread
"""The thread that runs the background worker."""
def __init__(self):
self.queue = Queue()
self.running = True
def attach(self, app: web.Application):
app.on_shutdown.append(self.on_shutdown)
LOG.debug("Starting background worker...")
self.thread = threading.Thread(target=self._run, daemon=True)
self.thread.start()
async def on_shutdown(self, _: web.Application):
self.running = False
try:
LOG.debug("Shutting down background worker...")
self.queue.put((CloseThread, (), {}))
self.thread.join(timeout=5)
LOG.debug("Background worker has been shut down.")
except Exception as e:
LOG.error(f"Failed to shut down background worker: {e}")
@staticmethod
def get_instance() -> "BackgroundWorker":
if BackgroundWorker._instance is None:
@ -40,15 +64,20 @@ class BackgroundWorker(metaclass=Singleton):
def _loop_runner():
try:
loop.run_forever()
except Exception as e:
LOG.exception("Loop error: %s", e)
except Exception:
pass
threading.Thread(target=_loop_runner, daemon=True, name="Background Runner").start()
loop_thread = threading.Thread(target=_loop_runner, daemon=True, name="Background Runner")
loop_thread.start()
while self.running:
try:
fn, args, kwargs = self.queue.get(timeout=1)
try:
if isinstance(fn, CloseThread):
LOG.info("Received shutdown signal for background worker.")
break
result = fn(*args, **kwargs)
if inspect.iscoroutine(result):
loop.call_soon_threadsafe(loop.create_task, result)
@ -58,9 +87,13 @@ class BackgroundWorker(metaclass=Singleton):
except Empty:
continue
try:
loop.call_soon_threadsafe(loop.stop)
loop_thread.join(timeout=5)
loop.close()
LOG.debug("Event loop has been stopped and closed.")
except Exception as e:
LOG.error(f"Failed to stop the event loop: {e!s}")
def submit(self, fn, *args, **kwargs):
self.queue.put((fn, args, kwargs))
def shutdown(self):
self.running = False
self.thread.join()

View file

@ -18,7 +18,6 @@ LOG = logging.getLogger("datastore")
class StoreType(str, Enum):
DONE = "done"
QUEUE = "queue"
PENDING = "pending"
@classmethod
def all(cls) -> list[str]:

View file

@ -331,7 +331,7 @@ class Download:
self.proc.start()
self.info.status = "preparing"
await self._notify.emit(Events.UPDATED, data=self.info)
await self._notify.emit(Events.ITEM_UPDATED, data=self.info)
asyncio.create_task(self.progress_update(), name=f"update-{self.id}")
return await asyncio.get_running_loop().run_in_executor(None, self.proc.join)
@ -465,7 +465,7 @@ class Download:
self.logger.debug(f"Status Update: {self.info._id=} {status=}")
if isinstance(status, str):
await self._notify.emit(Events.UPDATED, data=self.info)
await self._notify.emit(Events.ITEM_UPDATED, data=self.info)
continue
self.tmpfilename = status.get("tmpfilename")
@ -529,7 +529,7 @@ class Download:
self.logger.exception(e)
self.logger.error(f"Failed to run ffprobe. {status.get}. {e}")
await self._notify.emit(Events.UPDATED, data=self.info)
await self._notify.emit(Events.ITEM_UPDATED, data=self.info)
def is_stale(self) -> bool:
"""

View file

@ -65,9 +65,6 @@ class DownloadQueue(metaclass=Singleton):
done: DataStore
"""DataStore for the completed downloads."""
pending: DataStore
"""DataStore for the pending downloads."""
workers: asyncio.Semaphore
"""Semaphore to limit the number of concurrent downloads."""
@ -179,7 +176,15 @@ class DownloadQueue(metaclass=Singleton):
item.info.auto_start = True
updated = self.queue.put(item)
tasks.append(self._notify.emit(Events.UPDATED, data=updated.info))
tasks.append(self._notify.emit(Events.ITEM_UPDATED, data=updated.info))
tasks.append(
self._notify.emit(
Events.ITEM_RESUMED,
data=item.info,
title="Download Resumed",
message=f"Download '{item.info.title}' has been resumed.",
)
)
status[item_id] = "started"
started = True
LOG.debug(f"Item {item.info.name()} marked as started.")
@ -227,7 +232,15 @@ class DownloadQueue(metaclass=Singleton):
item.info.auto_start = False
updated = self.queue.put(item)
tasks.append(self._notify.emit(Events.UPDATED, data=updated.info))
tasks.append(self._notify.emit(Events.ITEM_UPDATED, data=updated.info))
tasks.append(
self._notify.emit(
Events.ITEM_PAUSED,
data=item.info,
title="Download Paused",
message=f"Download '{item.info.title}' has been paused.",
)
)
status[item_id] = "paused"
LOG.debug(f"Item {item.info.name()} marked as paused.")
@ -450,60 +463,56 @@ 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
nEvent: str | None = None
nTitle: str | None = None
nMessage: str | None = None
nStore: str = "queue"
text_logs: str = ""
if filtered_logs := extract_ytdlp_logs(logs):
text_logs = f" Logs: {', '.join(filtered_logs)}"
text_logs = ", ".join(filtered_logs)
if "is_upcoming" == entry.get("live_status"):
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}"
nEvent = Events.ITEM_MOVED
nStore = "history"
nTitle = "Upcoming Premiere" if is_premiere else "Upcoming Live Stream"
nMessage = 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 = notifyMessage.replace(f" '{dlInfo.info.title}'", "")
await self._notify.emit(
Events.LOG_INFO,
data={"lowPriority": True},
title=notifyTitle,
message=notifyMessage,
)
dlInfo.info.msg = nMessage.replace(f" '{dlInfo.info.title}'", "")
await self._notify.emit(Events.LOG_INFO, data={"lowPriority": True}, title=nTitle, message=nMessage)
itemDownload: Download = self.done.put(dlInfo)
elif len(entry.get("formats", [])) < 1:
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}'."
nTitle = "Download Error"
nMessage: str = f"No formats for '{dl.title}'."
nEvent = Events.ITEM_MOVED
nStore = "history"
dlInfo.info.error = notifyMessage.replace(f" for '{dl.title}'.", ".") + text_logs
if ava and ava not in ("public",):
nMessage += f" Availability is set for '{ava}'."
dlInfo.info.error = nMessage.replace(f" for '{dl.title}'.", ".") + text_logs
dlInfo.info.status = "error"
itemDownload = self.done.put(dlInfo)
notifyEvent = Events.COMPLETED
await self._notify.emit(
Events.LOG_WARNING,
data={"logs": text_logs},
title=notifyTitle,
message=notifyMessage,
)
await self._notify.emit(Events.LOG_WARNING, data={"logs": text_logs}, title=nTitle, message=nMessage)
elif is_premiere and self.config.prevent_live_premiere:
notifyTitle = "Premiere Video"
nStore = "history"
nTitle = "Premiere Video"
dlInfo.info.error = "Premiering right now."
_requeue = True
if release_in:
try:
starts_in = str_to_dt(release_in)
starts_in = (
starts_in: datetime = str_to_dt(release_in)
starts_in: datetime = (
starts_in.replace(tzinfo=UTC) if starts_in.tzinfo is None else starts_in.astimezone(UTC)
)
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()
nMessage = dlInfo.info.error.strip()
_requeue = False
except Exception as e:
LOG.error(f"Failed to parse live_in date '{release_in}'. {e!s}")
@ -511,24 +520,25 @@ class DownloadQueue(metaclass=Singleton):
else:
dlInfo.info.error += f" Delaying download by '{300+dl.extras.get('duration',0)}' seconds."
notifyMessage = dlInfo.info.error.strip()
nMessage = dlInfo.info.error.strip()
if _requeue:
notifyEvent = Events.ADDED
nEvent = Events.ITEM_ADDED
itemDownload = self.queue.put(dlInfo)
if item.auto_start:
self.event.set()
else:
dlInfo.info.status = "not_live"
itemDownload = self.done.put(dlInfo)
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)
nStore = "history"
nEvent = Events.ITEM_MOVED
nTitle = "Item Not Live"
nMessage = f"Item '{dlInfo.info.title}' is not live."
await self._notify.emit(Events.LOG_INFO, title=nTitle, message=nMessage)
else:
notifyEvent = Events.ADDED
notifyTitle = "Item Added"
notifyMessage = f"Item '{dlInfo.info.title}' has been added to the download queue."
nEvent = Events.ITEM_ADDED
nTitle = "Item Added"
nMessage = f"Item '{dlInfo.info.title}' has been added to the download queue."
itemDownload = self.queue.put(dlInfo)
if item.auto_start:
self.event.set()
@ -536,10 +546,10 @@ class DownloadQueue(metaclass=Singleton):
LOG.debug(f"Item {itemDownload.info.name()} is not set to auto-start.")
await self._notify.emit(
notifyEvent,
data=itemDownload.info.serialize(),
title=notifyTitle,
message=notifyMessage,
nEvent,
data={"to": nStore, "item": itemDownload.info} if Events.ITEM_MOVED == nEvent else itemDownload.info,
title=nTitle,
message=nMessage,
)
return {"status": "ok"}
@ -746,7 +756,7 @@ class DownloadQueue(metaclass=Singleton):
LOG.debug(f"Deleting from queue {item_ref}")
self.queue.delete(id)
await self._notify.emit(
Events.CANCELLED,
Events.ITEM_CANCELLED,
data=item.info,
title="Download Cancelled",
message=f"Download '{item.info.title}' has been cancelled.",
@ -754,8 +764,8 @@ class DownloadQueue(metaclass=Singleton):
item.info.status = "cancelled"
self.done.put(item)
await self._notify.emit(
Events.COMPLETED,
data=item.info,
Events.ITEM_MOVED,
data={"to": "history", "item": item.info},
title="Download Cancelled",
message=f"Download '{item.info.title}' has been cancelled.",
)
@ -829,7 +839,7 @@ class DownloadQueue(metaclass=Singleton):
_status: str = "Removed" if removed_files > 0 else "Cleared"
await self._notify.emit(
Events.CLEARED,
Events.ITEM_DELETED,
data=item.info,
title=f"Download {_status}",
message=f"{_status} '{item.info.title}' from history.",
@ -946,7 +956,7 @@ class DownloadQueue(metaclass=Singleton):
None
"""
filePath = calc_download_path(base_path=self.config.download_path, folder=entry.info.folder)
filePath: str = calc_download_path(base_path=self.config.download_path, folder=entry.info.folder)
LOG.info(f"Downloading 'id: {id}', 'Title: {entry.info.title}', 'URL: {entry.info.url}' To '{filePath}'.")
try:
@ -962,28 +972,35 @@ class DownloadQueue(metaclass=Singleton):
await entry.close()
if self.queue.exists(key=id):
_tasks = []
LOG.debug(f"Download Task '{id}' is completed. Removing from queue.")
self.queue.delete(key=id)
notifyTitle: str | None = None
notifyMessage: str | None = None
nTitle: str | None = None
nMessage: str | None = None
if entry.is_cancelled() is True:
await self._notify.emit(
Events.CANCELLED,
data=entry.info,
title="Download Cancelled",
message=f"Download '{entry.info.title}' has been cancelled.",
)
nTitle = "Download Cancelled"
nMessage = f"Download '{entry.info.title}' has been cancelled."
await self._notify.emit(Events.ITEM_CANCELLED, data=entry.info, title=nTitle, message=nMessage)
entry.info.status = "cancelled"
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."
if entry.info.status == "finished" and entry.info.filename:
nTitle = "Download Completed"
nMessage = f"Download '{entry.info.title}' has been finished."
_tasks.append(self._notify.emit(Events.ITEM_COMPLETED, data=entry.info, title=nTitle, message=nMessage))
self.done.put(value=entry)
await self._notify.emit(Events.COMPLETED, data=entry.info, title=notifyTitle, message=notifyMessage)
_tasks.append(
self._notify.emit(
Events.ITEM_MOVED,
data={"to": "history", "item": entry.info},
title=nTitle,
message=nMessage,
)
)
await asyncio.gather(*_tasks)
else:
LOG.warning(f"Download '{id}' not found in queue.")

View file

@ -17,53 +17,54 @@ class Events:
The events that can be emitted.
"""
STARTUP = "startup"
LOADED = "loaded"
STARTED = "started"
SHUTDOWN = "shutdown"
STARTUP: str = "startup"
LOADED: str = "loaded"
STARTED: str = "started"
SHUTDOWN: str = "shutdown"
ADDED = "added"
UPDATE = "update"
UPDATED = "updated"
COMPLETED = "completed"
CANCELLED = "cancelled"
CLEARED = "cleared"
CONNECTED = "connected"
STATUS = "status"
CONNECTED: str = "connected"
LOG_INFO = "log_info"
LOG_WARNING = "log_warning"
LOG_ERROR = "log_error"
LOG_SUCCESS = "log_success"
LOG_INFO: str = "log_info"
LOG_WARNING: str = "log_warning"
LOG_ERROR: str = "log_error"
LOG_SUCCESS: str = "log_success"
ITEM_DELETE = "item_delete"
ITEM_CANCEL = "item_cancel"
ITEM_ERROR = "item_error"
ITEM_ADDED: str = "item_added"
ITEM_UPDATED: str = "item_updated"
ITEM_COMPLETED: str = "item_completed"
ITEM_CANCELLED: str = "item_cancelled"
ITEM_DELETED: str = "item_deleted"
ITEM_PAUSED: str = "item_paused"
ITEM_RESUMED: str = "item_resumed"
ITEM_MOVED: str = "item_moved"
ITEM_STATUS: str = "item_status"
ITEM_ERROR: str = "item_error"
TEST = "test"
ADD_URL = "add_url"
TEST: str = "test"
ADD_URL: str = "add_url"
PAUSED = "paused"
PAUSED: str = "paused"
RESUMED: str = "resumed"
CLI_POST = "cli_post"
CLI_CLOSE = "cli_close"
CLI_OUTPUT = "cli_output"
CLI_POST: str = "cli_post"
CLI_CLOSE: str = "cli_close"
CLI_OUTPUT: str = "cli_output"
TASKS_ADD = "task_add"
TASK_DISPATCHED = "task_dispatched"
TASK_FINISHED = "task_finished"
TASK_ERROR = "task_error"
TASKS_ADD: str = "task_add"
TASK_DISPATCHED: str = "task_dispatched"
TASK_FINISHED: str = "task_finished"
TASK_ERROR: str = "task_error"
PRESETS_ADD = "presets_add"
PRESETS_UPDATE = "presets_update"
PRESETS_ADD: str = "presets_add"
PRESETS_UPDATE: str = "presets_update"
SCHEDULE_ADD = "schedule_add"
SCHEDULE_ADD: str = "schedule_add"
CONDITIONS_ADD = "conditions_add"
CONDITIONS_UPDATE = "conditions_update"
CONDITIONS_ADD: str = "conditions_add"
CONDITIONS_UPDATE: str = "conditions_update"
SUBSCRIBED = "subscribed"
UNSUBSCRIBED = "unsubscribed"
SUBSCRIBED: str = "subscribed"
UNSUBSCRIBED: str = "unsubscribed"
def get_all() -> list:
"""
@ -87,21 +88,22 @@ class Events:
"""
return [
Events.CONNECTED,
Events.ADDED,
Events.LOG_INFO,
Events.LOG_WARNING,
Events.LOG_ERROR,
Events.LOG_SUCCESS,
Events.COMPLETED,
Events.CANCELLED,
Events.CLEARED,
Events.UPDATED,
Events.UPDATE,
Events.ITEM_ADDED,
Events.ITEM_UPDATED,
Events.ITEM_COMPLETED,
Events.ITEM_CANCELLED,
Events.ITEM_DELETED,
Events.ITEM_MOVED,
Events.ITEM_STATUS,
Events.PAUSED,
Events.PRESETS_UPDATE,
Events.STATUS,
Events.RESUMED,
Events.CLI_CLOSE,
Events.CLI_OUTPUT,
Events.PRESETS_UPDATE,
]
def only_debug() -> list:
@ -112,7 +114,7 @@ class Events:
list: The list of debug events.
"""
return [Events.UPDATED, Events.CLI_OUTPUT]
return [Events.ITEM_UPDATED, Events.CLI_OUTPUT]
@dataclass(kw_only=True)

View file

@ -90,15 +90,25 @@ class Target:
class NotificationEvents:
ADDED = Events.ADDED
COMPLETED = Events.COMPLETED
CANCELLED = Events.CANCELLED
CLEARED = Events.CLEARED
LOG_INFO = Events.LOG_INFO
LOG_SUCCESS = Events.LOG_SUCCESS
LOG_WARNING = Events.LOG_WARNING
LOG_ERROR = Events.LOG_ERROR
TEST = Events.TEST
TEST: str = Events.TEST
ITEM_ADDED: str = Events.ITEM_ADDED
ITEM_COMPLETED: str = Events.ITEM_COMPLETED
ITEM_CANCELLED: str = Events.ITEM_CANCELLED
ITEM_DELETED: str = Events.ITEM_DELETED
ITEM_PAUSED: str = Events.ITEM_PAUSED
ITEM_RESUMED: str = Events.ITEM_RESUMED
ITEM_MOVED: str = Events.ITEM_MOVED
PAUSED: str = Events.PAUSED
RESUMED: str = Events.RESUMED
LOG_INFO: str = Events.LOG_INFO
LOG_SUCCESS: str = Events.LOG_SUCCESS
LOG_WARNING: str = Events.LOG_WARNING
LOG_ERROR: str = Events.LOG_ERROR
TASK_DISPATCHED: str = Events.TASK_DISPATCHED
@staticmethod
def get_events() -> dict[str, str]:

View file

@ -11,12 +11,13 @@ from typing import Any
from aiohttp import web
from app.library.Services import Services
from .config import Config
from .DownloadQueue import DownloadQueue
from .encoder import Encoder
from .Events import EventBus, Events
from .ItemDTO import Item
from .Scheduler import Scheduler
from .Services import Services
from .Singleton import Singleton
from .Utils import init_class, validate_url
@ -76,6 +77,7 @@ class Tasks(metaclass=Singleton):
self._scheduler: Scheduler = scheduler or Scheduler.get_instance()
self._notify: EventBus = EventBus.get_instance()
self._task_handler = HandleTask(self._scheduler, self, config)
self._downloadQueue = DownloadQueue.get_instance()
if self._file.exists() and "600" != self._file.stat().st_mode:
try:
@ -316,31 +318,38 @@ class Tasks(metaclass=Singleton):
template: str = task.template if task.template else ""
cli: str = task.cli if task.cli else ""
await self._notify.emit(
Events.ADD_URL,
data={
"url": task.url,
"preset": preset,
"folder": folder,
"template": template,
"cli": cli,
},
title="Tasks",
message=f"Task '{task.name}' started at '{timeNow}'",
id=task.id,
status = await self._downloadQueue.add(
item=Item.format(
{
"url": task.url,
"preset": preset,
"folder": folder,
"template": template,
"cli": cli,
}
)
)
timeNow = datetime.now(UTC).isoformat()
ended: float = time.time()
LOG.info(f"Task '{task.name}' completed at '{timeNow}' took '{ended - started:.2f}' seconds.")
await self._notify.emit(
Events.LOG_SUCCESS,
data={"lowPriority": True},
title="Task completed",
message=f"Task '{task.name}' completed in '{ended - started:.2f}'.",
)
_tasks = [
self._notify.emit(
Events.TASK_DISPATCHED,
data=status,
title=f"Task '{task.name}' dispatched",
message=f"Task '{task.name}' dispatched at '{timeNow}'.",
),
self._notify.emit(
Events.LOG_SUCCESS,
data={"lowPriority": True},
title="Task completed",
message=f"Task '{task.name}' completed in '{ended - started:.2f}'.",
),
]
await asyncio.gather(*_tasks)
except Exception as e:
LOG.error(f"Failed to execute '{task.name}' at '{timeNow}'. '{e!s}'.")
await self._notify.emit(

View file

@ -1188,6 +1188,8 @@ def extract_ytdlp_logs(logs: list[str], filters: list[str | re.Pattern] = None)
"This video is available to this channel",
"Private video. Sign in if you've been granted access to this video",
"[youtube] Premieres in",
"Falling back on generic information extractor",
"URL could be a direct video link, returning it as such",
] + (filters or [])
compiled: list[re.Pattern] = [

View file

@ -7,7 +7,7 @@ from typing import TYPE_CHECKING
from app.library.config import Config
from app.library.DownloadQueue import DownloadQueue
from app.library.Events import EventBus, Events
from app.library.ItemDTO import ItemDTO
from app.library.ItemDTO import Item, ItemDTO
from app.library.Tasks import Task
from app.library.Utils import is_downloaded
from app.library.YTDLPOpts import YTDLPOpts
@ -147,18 +147,18 @@ class YoutubeHandler:
template: str = task.template if task.template else ""
cli: str = task.cli if task.cli else ""
queued = asyncio.gather(
*[
notify.emit(
Events.ADD_URL,
data={"url": item["url"], "preset": preset, "folder": folder, "template": template, "cli": cli},
)
for item in filtered
]
)
try:
await queued
await asyncio.gather(
*[
notify.emit(
Events.ADD_URL,
data=Item.format(
{"url": item["url"], "preset": preset, "folder": folder, "template": template, "cli": cli}
).serialize(),
)
for item in filtered
]
)
except Exception as e:
LOG.error(f"Error while adding items from '{task.id}: {task.name}'. {e!s}")
return

View file

@ -127,6 +127,7 @@ class Main:
Presets.get_instance().attach(self._app)
Notification.get_instance().attach(self._app)
Conditions.get_instance().attach(self._app)
self._background_worker.attach(self._app)
EventBus.get_instance().sync_emit(
Events.LOADED,

View file

@ -137,7 +137,7 @@ async def item_update(request: Request, queue: DownloadQueue, encoder: Encoder,
if updated:
queue.done.put(item)
await notify.emit(Events.UPDATE, data=item.info)
await notify.emit(Events.ITEM_UPDATED, data=item.info)
return web.json_response(
data=item.info,

View file

@ -23,7 +23,7 @@ async def pause(notify: EventBus, queue: DownloadQueue):
Events.PAUSED,
data={"paused": True, "at": time.time()},
title="Downloads Paused",
message="Download pool has been paused.",
message="Non-active downloads have been paused.",
)
@ -31,10 +31,10 @@ async def pause(notify: EventBus, queue: DownloadQueue):
async def resume(notify: EventBus, queue: DownloadQueue):
queue.resume()
await notify.emit(
Events.PAUSED,
Events.RESUMED,
data={"paused": False, "at": time.time()},
title="Downloads Resumed",
message="Download pool has been resumed.",
message="Resumed all downloads.",
)
@ -54,7 +54,7 @@ async def add_url(queue: DownloadQueue, notify: EventBus, sid: str, data: dict):
try:
status = await queue.add(item=Item.format(data))
await notify.emit(
event=Events.STATUS,
event=Events.ITEM_STATUS,
title="Adding URL",
message=f"Adding URL '{url}' to the download queue.",
data=status,
@ -76,12 +76,26 @@ async def item_cancel(queue: DownloadQueue, notify: EventBus, sid: str, data: st
)
return
try:
item = queue.get_item(id=data)
except KeyError:
await notify.emit(
Events.LOG_ERROR,
title="Item Not Found",
message=f"Item with ID '{data}' not found.",
to=sid,
)
return
status: dict[str, str] = {}
status = await queue.cancel([data])
status.update({"identifier": data})
await notify.emit(
Events.ITEM_CANCEL, data=status, title="Item Cancelled", message=f"Item '{data}': has been cancelled."
Events.ITEM_CANCELLED,
data=item.info,
title="Item Cancelled",
message=f"Cancelled '{item.info.title}'.",
)
@ -106,23 +120,7 @@ async def item_delete(queue: DownloadQueue, notify: EventBus, sid: str, data: di
)
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] = {}
status = await queue.clear([id], remove_file=bool(data.get("remove_file", False)))
status.update({"identifier": id})
await notify.emit(
Events.ITEM_DELETE, data=status, title="Item Deleted", message=f"Item '{item.info.title}': has been deleted."
)
await queue.clear([id], remove_file=bool(data.get("remove_file", False)))
@route(RouteType.SOCKET, "archive_item", "archive_item")

View file

@ -4,25 +4,25 @@
"type": "module",
"scripts": {
"build": "nuxt build",
"dev": "nuxt dev",
"dev": "nuxt dev --host",
"generate": "nuxt generate",
"preview": "nuxt preview",
"postinstall": "nuxt prepare"
},
"web-types": "./web-types.json",
"dependencies": {
"@pinia/nuxt": "^0.11.1",
"@sentry/nuxt": "^9.35.0",
"@pinia/nuxt": "^0.11.2",
"@sentry/nuxt": "^9.40.0",
"@vueuse/core": "^13.5.0",
"@vueuse/nuxt": "^13.5.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",
"cron-parser": "^5.3.0",
"cronstrue": "^3.0.0",
"cronstrue": "^3.1.0",
"floating-vue": "^5.2.2",
"hls.js": "^1.6.7",
"moment": "^2.30.1",
"nuxt": "^3.17.6",
"nuxt": "^4.0.0",
"pinia": "^3.0.3",
"socket.io-client": "^4.8.1",
"vue": "^3.5.17",

View file

@ -89,7 +89,7 @@ watch(() => config.app.basic_mode, async () => {
return
}
await navigateTo('/')
}, { immediate: true })
})
watch(() => config.app.console_enabled, async () => {
if (config.app.console_enabled) {
@ -97,7 +97,7 @@ watch(() => config.app.console_enabled, async () => {
}
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) {
@ -111,6 +111,12 @@ const runCommand = async () => {
return
}
if (config.app.basic_mode || !config.app.console_enabled) {
await navigateTo('/')
toast.error('Console is disabled in the configuration. Please enable it to use this feature.')
return
}
if (command.value.startsWith('yt-dlp')) {
command.value = command.value.replace(/^yt-dlp/, '').trim()
await nextTick()

View file

@ -397,7 +397,7 @@ watch(() => config.app.basic_mode, async () => {
watch(() => socket.isConnected, async () => {
if (socket.isConnected && initialLoad.value) {
socket.on('status', statusHandler)
socket.on('item_status', statusHandler)
await reloadContent(true)
initialLoad.value = false
}
@ -586,7 +586,7 @@ onMounted(async () => {
if (!socket.isConnected) {
return;
}
socket.on('status', statusHandler)
socket.on('item_status', statusHandler)
await reloadContent(true)
});
@ -683,7 +683,7 @@ const runNow = async (item: task_item, mass: boolean = false) => {
}, 500)
}
onBeforeUnmount(() => socket.off('status', statusHandler))
onBeforeUnmount(() => socket.off('item_status', statusHandler))
const statusHandler = async (stream: string) => {
const json = JSON.parse(stream)

File diff suppressed because it is too large Load diff

View file

@ -12,6 +12,21 @@ export const useSocketStore = defineStore('socket', () => {
const socket = ref<IOSocket | null>(null)
const isConnected = ref<boolean>(false)
const emit = (event: string, data?: any): any => socket.value?.emit(event, data)
const on = (event: string | string[], callback: (...args: any[]) => void, withEvent: boolean = false) => {
if (!Array.isArray(event)) {
event = [event]
}
event.forEach(e => socket.value?.on(e, (...args) => true === withEvent ? callback(e, ...args) : callback(...args)))
}
const off = (event: string | string[], callback: (...args: any[]) => void): any => {
if (!Array.isArray(event)) {
event = [event]
}
event.forEach(e => socket.value?.off(e, callback));
}
const connect = () => {
let opts = {
transports: ['websocket', 'polling'],
@ -23,6 +38,8 @@ export const useSocketStore = defineStore('socket', () => {
if ('development' !== runtimeConfig.public?.APP_ENV) {
url = window.origin;
opts.path = `${runtimeConfig.app.baseURL.replace(/\/$/, '')}/socket.io`;
} else {
window.ws = socket.value;
}
socket.value = io(url, opts)
@ -45,13 +62,13 @@ export const useSocketStore = defineStore('socket', () => {
stateStore.addAll('history', json.data.done || {})
})
socket.value.on('added', stream => {
on('item_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 => {
on(['log_info', 'log_success', 'log_warning', 'log_error'], (event: string, stream: string) => {
const json = JSON.parse(stream);
const message = json?.message || json?.data?.message;
const data = json.data?.data || json.data || {};
@ -69,19 +86,24 @@ export const useSocketStore = defineStore('socket', () => {
toast.error(message, data);
break;
}
}));
}, true);
socket.value.on('completed', stream => {
on('item_completed', (stream: string) => {
const json = JSON.parse(stream);
if (true === stateStore.has('queue', json.data._id)) {
stateStore.remove('queue', json.data._id);
}
if (true === stateStore.has('history', json.data._id)) {
stateStore.update('history', json.data._id, json.data);
return;
}
stateStore.add('history', json.data._id, json.data);
});
socket.value.on('cancelled', stream => {
on('item_cancelled', (stream: string) => {
const item = JSON.parse(stream);
const id = item.data._id
@ -96,7 +118,7 @@ export const useSocketStore = defineStore('socket', () => {
}
});
socket.value.on('cleared', stream => {
on('item_deleted', (stream: string) => {
const item = JSON.parse(stream);
const id = item.data._id
@ -107,7 +129,7 @@ export const useSocketStore = defineStore('socket', () => {
stateStore.remove('history', id);
});
socket.value.on("updated", stream => {
on('item_updated', (stream: string) => {
const json = JSON.parse(stream);
const id = json.data._id;
@ -116,42 +138,51 @@ export const useSocketStore = defineStore('socket', () => {
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;
if (true === stateStore.has('queue', id)) {
stateStore.update('queue', id, json.data);
}
});
socket.value.on('paused', data => {
on('item_moved', (stream: string) => {
const json = JSON.parse(stream);
const to = json.data.to;
const id = json.data.item._id;
if ('queue' === to) {
if (true === stateStore.has('history', id)) {
stateStore.remove('history', id);
}
stateStore.add('queue', id, json.data.item);
}
if ('history' === to) {
if (true === stateStore.has('queue', id)) {
stateStore.remove('queue', id);
}
stateStore.add('history', id, json.data.item);
}
});
on(['paused', 'resumed'], (event: string, data: string) => {
const json = JSON.parse(data);
const pausedState = Boolean(json.data.paused);
config.update('paused', pausedState);
if (false === pausedState) {
if ('resumed' === event) {
toast.success('Download queue resumed.');
return;
}
toast.warning('Download queue paused.', { timeout: 10000 });
});
}, true);
socket.value.on('presets_update', (data: string) => config.update('presets', JSON.parse(data).data || []));
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 };
});

148
uv.lock
View file

@ -27,7 +27,7 @@ wheels = [
[[package]]
name = "aiohttp"
version = "3.12.13"
version = "3.12.14"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "aiohappyeyeballs" },
@ -38,59 +38,59 @@ dependencies = [
{ name = "propcache" },
{ name = "yarl" },
]
sdist = { url = "https://files.pythonhosted.org/packages/42/6e/ab88e7cb2a4058bed2f7870276454f85a7c56cd6da79349eb314fc7bbcaa/aiohttp-3.12.13.tar.gz", hash = "sha256:47e2da578528264a12e4e3dd8dd72a7289e5f812758fe086473fab037a10fcce", size = 7819160 }
sdist = { url = "https://files.pythonhosted.org/packages/e6/0b/e39ad954107ebf213a2325038a3e7a506be3d98e1435e1f82086eec4cde2/aiohttp-3.12.14.tar.gz", hash = "sha256:6e06e120e34d93100de448fd941522e11dafa78ef1a893c179901b7d66aa29f2", size = 7822921 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6a/65/5566b49553bf20ffed6041c665a5504fb047cefdef1b701407b8ce1a47c4/aiohttp-3.12.13-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7c229b1437aa2576b99384e4be668af1db84b31a45305d02f61f5497cfa6f60c", size = 709401 },
{ url = "https://files.pythonhosted.org/packages/14/b5/48e4cc61b54850bdfafa8fe0b641ab35ad53d8e5a65ab22b310e0902fa42/aiohttp-3.12.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04076d8c63471e51e3689c93940775dc3d12d855c0c80d18ac5a1c68f0904358", size = 481669 },
{ url = "https://files.pythonhosted.org/packages/04/4f/e3f95c8b2a20a0437d51d41d5ccc4a02970d8ad59352efb43ea2841bd08e/aiohttp-3.12.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55683615813ce3601640cfaa1041174dc956d28ba0511c8cbd75273eb0587014", size = 469933 },
{ url = "https://files.pythonhosted.org/packages/41/c9/c5269f3b6453b1cfbd2cfbb6a777d718c5f086a3727f576c51a468b03ae2/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:921bc91e602d7506d37643e77819cb0b840d4ebb5f8d6408423af3d3bf79a7b7", size = 1740128 },
{ url = "https://files.pythonhosted.org/packages/6f/49/a3f76caa62773d33d0cfaa842bdf5789a78749dbfe697df38ab1badff369/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e72d17fe0974ddeae8ed86db297e23dba39c7ac36d84acdbb53df2e18505a013", size = 1688796 },
{ url = "https://files.pythonhosted.org/packages/ad/e4/556fccc4576dc22bf18554b64cc873b1a3e5429a5bdb7bbef7f5d0bc7664/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0653d15587909a52e024a261943cf1c5bdc69acb71f411b0dd5966d065a51a47", size = 1787589 },
{ url = "https://files.pythonhosted.org/packages/b9/3d/d81b13ed48e1a46734f848e26d55a7391708421a80336e341d2aef3b6db2/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a77b48997c66722c65e157c06c74332cdf9c7ad00494b85ec43f324e5c5a9b9a", size = 1826635 },
{ url = "https://files.pythonhosted.org/packages/75/a5/472e25f347da88459188cdaadd1f108f6292f8a25e62d226e63f860486d1/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6946bae55fd36cfb8e4092c921075cde029c71c7cb571d72f1079d1e4e013bc", size = 1729095 },
{ url = "https://files.pythonhosted.org/packages/b9/fe/322a78b9ac1725bfc59dfc301a5342e73d817592828e4445bd8f4ff83489/aiohttp-3.12.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f95db8c8b219bcf294a53742c7bda49b80ceb9d577c8e7aa075612b7f39ffb7", size = 1666170 },
{ url = "https://files.pythonhosted.org/packages/7a/77/ec80912270e231d5e3839dbd6c065472b9920a159ec8a1895cf868c2708e/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03d5eb3cfb4949ab4c74822fb3326cd9655c2b9fe22e4257e2100d44215b2e2b", size = 1714444 },
{ url = "https://files.pythonhosted.org/packages/21/b2/fb5aedbcb2b58d4180e58500e7c23ff8593258c27c089abfbcc7db65bd40/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6383dd0ffa15515283c26cbf41ac8e6705aab54b4cbb77bdb8935a713a89bee9", size = 1709604 },
{ url = "https://files.pythonhosted.org/packages/e3/15/a94c05f7c4dc8904f80b6001ad6e07e035c58a8ebfcc15e6b5d58500c858/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6548a411bc8219b45ba2577716493aa63b12803d1e5dc70508c539d0db8dbf5a", size = 1689786 },
{ url = "https://files.pythonhosted.org/packages/1d/fd/0d2e618388f7a7a4441eed578b626bda9ec6b5361cd2954cfc5ab39aa170/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:81b0fcbfe59a4ca41dc8f635c2a4a71e63f75168cc91026c61be665945739e2d", size = 1783389 },
{ url = "https://files.pythonhosted.org/packages/a6/6b/6986d0c75996ef7e64ff7619b9b7449b1d1cbbe05c6755e65d92f1784fe9/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6a83797a0174e7995e5edce9dcecc517c642eb43bc3cba296d4512edf346eee2", size = 1803853 },
{ url = "https://files.pythonhosted.org/packages/21/65/cd37b38f6655d95dd07d496b6d2f3924f579c43fd64b0e32b547b9c24df5/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5734d8469a5633a4e9ffdf9983ff7cdb512524645c7a3d4bc8a3de45b935ac3", size = 1716909 },
{ url = "https://files.pythonhosted.org/packages/fd/20/2de7012427dc116714c38ca564467f6143aec3d5eca3768848d62aa43e62/aiohttp-3.12.13-cp311-cp311-win32.whl", hash = "sha256:fef8d50dfa482925bb6b4c208b40d8e9fa54cecba923dc65b825a72eed9a5dbd", size = 427036 },
{ url = "https://files.pythonhosted.org/packages/f8/b6/98518bcc615ef998a64bef371178b9afc98ee25895b4f476c428fade2220/aiohttp-3.12.13-cp311-cp311-win_amd64.whl", hash = "sha256:9a27da9c3b5ed9d04c36ad2df65b38a96a37e9cfba6f1381b842d05d98e6afe9", size = 451427 },
{ url = "https://files.pythonhosted.org/packages/b4/6a/ce40e329788013cd190b1d62bbabb2b6a9673ecb6d836298635b939562ef/aiohttp-3.12.13-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0aa580cf80558557285b49452151b9c69f2fa3ad94c5c9e76e684719a8791b73", size = 700491 },
{ url = "https://files.pythonhosted.org/packages/28/d9/7150d5cf9163e05081f1c5c64a0cdf3c32d2f56e2ac95db2a28fe90eca69/aiohttp-3.12.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b103a7e414b57e6939cc4dece8e282cfb22043efd0c7298044f6594cf83ab347", size = 475104 },
{ url = "https://files.pythonhosted.org/packages/f8/91/d42ba4aed039ce6e449b3e2db694328756c152a79804e64e3da5bc19dffc/aiohttp-3.12.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f64e748e9e741d2eccff9597d09fb3cd962210e5b5716047cbb646dc8fe06f", size = 467948 },
{ url = "https://files.pythonhosted.org/packages/99/3b/06f0a632775946981d7c4e5a865cddb6e8dfdbaed2f56f9ade7bb4a1039b/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c955989bf4c696d2ededc6b0ccb85a73623ae6e112439398935362bacfaaf6", size = 1714742 },
{ url = "https://files.pythonhosted.org/packages/92/a6/2552eebad9ec5e3581a89256276009e6a974dc0793632796af144df8b740/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d640191016763fab76072c87d8854a19e8e65d7a6fcfcbf017926bdbbb30a7e5", size = 1697393 },
{ url = "https://files.pythonhosted.org/packages/d8/9f/bd08fdde114b3fec7a021381b537b21920cdd2aa29ad48c5dffd8ee314f1/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dc507481266b410dede95dd9f26c8d6f5a14315372cc48a6e43eac652237d9b", size = 1752486 },
{ url = "https://files.pythonhosted.org/packages/f7/e1/affdea8723aec5bd0959171b5490dccd9a91fcc505c8c26c9f1dca73474d/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8a94daa873465d518db073bd95d75f14302e0208a08e8c942b2f3f1c07288a75", size = 1798643 },
{ url = "https://files.pythonhosted.org/packages/f3/9d/666d856cc3af3a62ae86393baa3074cc1d591a47d89dc3bf16f6eb2c8d32/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f52420cde4ce0bb9425a375d95577fe082cb5721ecb61da3049b55189e4e6", size = 1718082 },
{ url = "https://files.pythonhosted.org/packages/f3/ce/3c185293843d17be063dada45efd2712bb6bf6370b37104b4eda908ffdbd/aiohttp-3.12.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f7df1f620ec40f1a7fbcb99ea17d7326ea6996715e78f71a1c9a021e31b96b8", size = 1633884 },
{ url = "https://files.pythonhosted.org/packages/3a/5b/f3413f4b238113be35dfd6794e65029250d4b93caa0974ca572217745bdb/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3062d4ad53b36e17796dce1c0d6da0ad27a015c321e663657ba1cc7659cfc710", size = 1694943 },
{ url = "https://files.pythonhosted.org/packages/82/c8/0e56e8bf12081faca85d14a6929ad5c1263c146149cd66caa7bc12255b6d/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8605e22d2a86b8e51ffb5253d9045ea73683d92d47c0b1438e11a359bdb94462", size = 1716398 },
{ url = "https://files.pythonhosted.org/packages/ea/f3/33192b4761f7f9b2f7f4281365d925d663629cfaea093a64b658b94fc8e1/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:54fbbe6beafc2820de71ece2198458a711e224e116efefa01b7969f3e2b3ddae", size = 1657051 },
{ url = "https://files.pythonhosted.org/packages/5e/0b/26ddd91ca8f84c48452431cb4c5dd9523b13bc0c9766bda468e072ac9e29/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:050bd277dfc3768b606fd4eae79dd58ceda67d8b0b3c565656a89ae34525d15e", size = 1736611 },
{ url = "https://files.pythonhosted.org/packages/c3/8d/e04569aae853302648e2c138a680a6a2f02e374c5b6711732b29f1e129cc/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2637a60910b58f50f22379b6797466c3aa6ae28a6ab6404e09175ce4955b4e6a", size = 1764586 },
{ url = "https://files.pythonhosted.org/packages/ac/98/c193c1d1198571d988454e4ed75adc21c55af247a9fda08236602921c8c8/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e986067357550d1aaa21cfe9897fa19e680110551518a5a7cf44e6c5638cb8b5", size = 1724197 },
{ url = "https://files.pythonhosted.org/packages/e7/9e/07bb8aa11eec762c6b1ff61575eeeb2657df11ab3d3abfa528d95f3e9337/aiohttp-3.12.13-cp312-cp312-win32.whl", hash = "sha256:ac941a80aeea2aaae2875c9500861a3ba356f9ff17b9cb2dbfb5cbf91baaf5bf", size = 421771 },
{ url = "https://files.pythonhosted.org/packages/52/66/3ce877e56ec0813069cdc9607cd979575859c597b6fb9b4182c6d5f31886/aiohttp-3.12.13-cp312-cp312-win_amd64.whl", hash = "sha256:671f41e6146a749b6c81cb7fd07f5a8356d46febdaaaf07b0e774ff04830461e", size = 447869 },
{ url = "https://files.pythonhosted.org/packages/11/0f/db19abdf2d86aa1deec3c1e0e5ea46a587b97c07a16516b6438428b3a3f8/aiohttp-3.12.13-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d4a18e61f271127465bdb0e8ff36e8f02ac4a32a80d8927aa52371e93cd87938", size = 694910 },
{ url = "https://files.pythonhosted.org/packages/d5/81/0ab551e1b5d7f1339e2d6eb482456ccbe9025605b28eed2b1c0203aaaade/aiohttp-3.12.13-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:532542cb48691179455fab429cdb0d558b5e5290b033b87478f2aa6af5d20ace", size = 472566 },
{ url = "https://files.pythonhosted.org/packages/34/3f/6b7d336663337672d29b1f82d1f252ec1a040fe2d548f709d3f90fa2218a/aiohttp-3.12.13-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d7eea18b52f23c050ae9db5d01f3d264ab08f09e7356d6f68e3f3ac2de9dfabb", size = 464856 },
{ url = "https://files.pythonhosted.org/packages/26/7f/32ca0f170496aa2ab9b812630fac0c2372c531b797e1deb3deb4cea904bd/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad7c8e5c25f2a26842a7c239de3f7b6bfb92304593ef997c04ac49fb703ff4d7", size = 1703683 },
{ url = "https://files.pythonhosted.org/packages/ec/53/d5513624b33a811c0abea8461e30a732294112318276ce3dbf047dbd9d8b/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6af355b483e3fe9d7336d84539fef460120c2f6e50e06c658fe2907c69262d6b", size = 1684946 },
{ url = "https://files.pythonhosted.org/packages/37/72/4c237dd127827b0247dc138d3ebd49c2ded6114c6991bbe969058575f25f/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a95cf9f097498f35c88e3609f55bb47b28a5ef67f6888f4390b3d73e2bac6177", size = 1737017 },
{ url = "https://files.pythonhosted.org/packages/0d/67/8a7eb3afa01e9d0acc26e1ef847c1a9111f8b42b82955fcd9faeb84edeb4/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8ed8c38a1c584fe99a475a8f60eefc0b682ea413a84c6ce769bb19a7ff1c5ef", size = 1786390 },
{ url = "https://files.pythonhosted.org/packages/48/19/0377df97dd0176ad23cd8cad4fd4232cfeadcec6c1b7f036315305c98e3f/aiohttp-3.12.13-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a0b9170d5d800126b5bc89d3053a2363406d6e327afb6afaeda2d19ee8bb103", size = 1708719 },
{ url = "https://files.pythonhosted.org/packages/61/97/ade1982a5c642b45f3622255173e40c3eed289c169f89d00eeac29a89906/aiohttp-3.12.13-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:372feeace612ef8eb41f05ae014a92121a512bd5067db8f25101dd88a8db11da", size = 1622424 },
{ url = "https://files.pythonhosted.org/packages/99/ab/00ad3eea004e1d07ccc406e44cfe2b8da5acb72f8c66aeeb11a096798868/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a946d3702f7965d81f7af7ea8fb03bb33fe53d311df48a46eeca17e9e0beed2d", size = 1675447 },
{ url = "https://files.pythonhosted.org/packages/3f/fe/74e5ce8b2ccaba445fe0087abc201bfd7259431d92ae608f684fcac5d143/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a0c4725fae86555bbb1d4082129e21de7264f4ab14baf735278c974785cd2041", size = 1707110 },
{ url = "https://files.pythonhosted.org/packages/ef/c4/39af17807f694f7a267bd8ab1fbacf16ad66740862192a6c8abac2bff813/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b28ea2f708234f0a5c44eb6c7d9eb63a148ce3252ba0140d050b091b6e842d1", size = 1649706 },
{ url = "https://files.pythonhosted.org/packages/38/e8/f5a0a5f44f19f171d8477059aa5f28a158d7d57fe1a46c553e231f698435/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d4f5becd2a5791829f79608c6f3dc745388162376f310eb9c142c985f9441cc1", size = 1725839 },
{ url = "https://files.pythonhosted.org/packages/fd/ac/81acc594c7f529ef4419d3866913f628cd4fa9cab17f7bf410a5c3c04c53/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:60f2ce6b944e97649051d5f5cc0f439360690b73909230e107fd45a359d3e911", size = 1759311 },
{ url = "https://files.pythonhosted.org/packages/38/0d/aabe636bd25c6ab7b18825e5a97d40024da75152bec39aa6ac8b7a677630/aiohttp-3.12.13-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:69fc1909857401b67bf599c793f2183fbc4804717388b0b888f27f9929aa41f3", size = 1708202 },
{ url = "https://files.pythonhosted.org/packages/1f/ab/561ef2d8a223261683fb95a6283ad0d36cb66c87503f3a7dde7afe208bb2/aiohttp-3.12.13-cp313-cp313-win32.whl", hash = "sha256:7d7e68787a2046b0e44ba5587aa723ce05d711e3a3665b6b7545328ac8e3c0dd", size = 420794 },
{ url = "https://files.pythonhosted.org/packages/9d/47/b11d0089875a23bff0abd3edb5516bcd454db3fefab8604f5e4b07bd6210/aiohttp-3.12.13-cp313-cp313-win_amd64.whl", hash = "sha256:5a178390ca90419bfd41419a809688c368e63c86bd725e1186dd97f6b89c2706", size = 446735 },
{ url = "https://files.pythonhosted.org/packages/53/e1/8029b29316971c5fa89cec170274582619a01b3d82dd1036872acc9bc7e8/aiohttp-3.12.14-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f4552ff7b18bcec18b60a90c6982049cdb9dac1dba48cf00b97934a06ce2e597", size = 709960 },
{ url = "https://files.pythonhosted.org/packages/96/bd/4f204cf1e282041f7b7e8155f846583b19149e0872752711d0da5e9cc023/aiohttp-3.12.14-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8283f42181ff6ccbcf25acaae4e8ab2ff7e92b3ca4a4ced73b2c12d8cd971393", size = 482235 },
{ url = "https://files.pythonhosted.org/packages/d6/0f/2a580fcdd113fe2197a3b9df30230c7e85bb10bf56f7915457c60e9addd9/aiohttp-3.12.14-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:040afa180ea514495aaff7ad34ec3d27826eaa5d19812730fe9e529b04bb2179", size = 470501 },
{ url = "https://files.pythonhosted.org/packages/38/78/2c1089f6adca90c3dd74915bafed6d6d8a87df5e3da74200f6b3a8b8906f/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b413c12f14c1149f0ffd890f4141a7471ba4b41234fe4fd4a0ff82b1dc299dbb", size = 1740696 },
{ url = "https://files.pythonhosted.org/packages/4a/c8/ce6c7a34d9c589f007cfe064da2d943b3dee5aabc64eaecd21faf927ab11/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1d6f607ce2e1a93315414e3d448b831238f1874b9968e1195b06efaa5c87e245", size = 1689365 },
{ url = "https://files.pythonhosted.org/packages/18/10/431cd3d089de700756a56aa896faf3ea82bee39d22f89db7ddc957580308/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:565e70d03e924333004ed101599902bba09ebb14843c8ea39d657f037115201b", size = 1788157 },
{ url = "https://files.pythonhosted.org/packages/fa/b2/26f4524184e0f7ba46671c512d4b03022633bcf7d32fa0c6f1ef49d55800/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4699979560728b168d5ab63c668a093c9570af2c7a78ea24ca5212c6cdc2b641", size = 1827203 },
{ url = "https://files.pythonhosted.org/packages/e0/30/aadcdf71b510a718e3d98a7bfeaea2396ac847f218b7e8edb241b09bd99a/aiohttp-3.12.14-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad5fdf6af93ec6c99bf800eba3af9a43d8bfd66dce920ac905c817ef4a712afe", size = 1729664 },
{ url = "https://files.pythonhosted.org/packages/67/7f/7ccf11756ae498fdedc3d689a0c36ace8fc82f9d52d3517da24adf6e9a74/aiohttp-3.12.14-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4ac76627c0b7ee0e80e871bde0d376a057916cb008a8f3ffc889570a838f5cc7", size = 1666741 },
{ url = "https://files.pythonhosted.org/packages/6b/4d/35ebc170b1856dd020c92376dbfe4297217625ef4004d56587024dc2289c/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:798204af1180885651b77bf03adc903743a86a39c7392c472891649610844635", size = 1715013 },
{ url = "https://files.pythonhosted.org/packages/7b/24/46dc0380146f33e2e4aa088b92374b598f5bdcde1718c77e8d1a0094f1a4/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:4f1205f97de92c37dd71cf2d5bcfb65fdaed3c255d246172cce729a8d849b4da", size = 1710172 },
{ url = "https://files.pythonhosted.org/packages/2f/0a/46599d7d19b64f4d0fe1b57bdf96a9a40b5c125f0ae0d8899bc22e91fdce/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:76ae6f1dd041f85065d9df77c6bc9c9703da9b5c018479d20262acc3df97d419", size = 1690355 },
{ url = "https://files.pythonhosted.org/packages/08/86/b21b682e33d5ca317ef96bd21294984f72379454e689d7da584df1512a19/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a194ace7bc43ce765338ca2dfb5661489317db216ea7ea700b0332878b392cab", size = 1783958 },
{ url = "https://files.pythonhosted.org/packages/4f/45/f639482530b1396c365f23c5e3b1ae51c9bc02ba2b2248ca0c855a730059/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:16260e8e03744a6fe3fcb05259eeab8e08342c4c33decf96a9dad9f1187275d0", size = 1804423 },
{ url = "https://files.pythonhosted.org/packages/7e/e5/39635a9e06eed1d73671bd4079a3caf9cf09a49df08490686f45a710b80e/aiohttp-3.12.14-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:8c779e5ebbf0e2e15334ea404fcce54009dc069210164a244d2eac8352a44b28", size = 1717479 },
{ url = "https://files.pythonhosted.org/packages/51/e1/7f1c77515d369b7419c5b501196526dad3e72800946c0099594c1f0c20b4/aiohttp-3.12.14-cp311-cp311-win32.whl", hash = "sha256:a289f50bf1bd5be227376c067927f78079a7bdeccf8daa6a9e65c38bae14324b", size = 427907 },
{ url = "https://files.pythonhosted.org/packages/06/24/a6bf915c85b7a5b07beba3d42b3282936b51e4578b64a51e8e875643c276/aiohttp-3.12.14-cp311-cp311-win_amd64.whl", hash = "sha256:0b8a69acaf06b17e9c54151a6c956339cf46db4ff72b3ac28516d0f7068f4ced", size = 452334 },
{ url = "https://files.pythonhosted.org/packages/c3/0d/29026524e9336e33d9767a1e593ae2b24c2b8b09af7c2bd8193762f76b3e/aiohttp-3.12.14-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a0ecbb32fc3e69bc25efcda7d28d38e987d007096cbbeed04f14a6662d0eee22", size = 701055 },
{ url = "https://files.pythonhosted.org/packages/0a/b8/a5e8e583e6c8c1056f4b012b50a03c77a669c2e9bf012b7cf33d6bc4b141/aiohttp-3.12.14-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0400f0ca9bb3e0b02f6466421f253797f6384e9845820c8b05e976398ac1d81a", size = 475670 },
{ url = "https://files.pythonhosted.org/packages/29/e8/5202890c9e81a4ec2c2808dd90ffe024952e72c061729e1d49917677952f/aiohttp-3.12.14-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a56809fed4c8a830b5cae18454b7464e1529dbf66f71c4772e3cfa9cbec0a1ff", size = 468513 },
{ url = "https://files.pythonhosted.org/packages/23/e5/d11db8c23d8923d3484a27468a40737d50f05b05eebbb6288bafcb467356/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27f2e373276e4755691a963e5d11756d093e346119f0627c2d6518208483fb6d", size = 1715309 },
{ url = "https://files.pythonhosted.org/packages/53/44/af6879ca0eff7a16b1b650b7ea4a827301737a350a464239e58aa7c387ef/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ca39e433630e9a16281125ef57ece6817afd1d54c9f1bf32e901f38f16035869", size = 1697961 },
{ url = "https://files.pythonhosted.org/packages/bb/94/18457f043399e1ec0e59ad8674c0372f925363059c276a45a1459e17f423/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c748b3f8b14c77720132b2510a7d9907a03c20ba80f469e58d5dfd90c079a1c", size = 1753055 },
{ url = "https://files.pythonhosted.org/packages/26/d9/1d3744dc588fafb50ff8a6226d58f484a2242b5dd93d8038882f55474d41/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0a568abe1b15ce69d4cc37e23020720423f0728e3cb1f9bcd3f53420ec3bfe7", size = 1799211 },
{ url = "https://files.pythonhosted.org/packages/73/12/2530fb2b08773f717ab2d249ca7a982ac66e32187c62d49e2c86c9bba9b4/aiohttp-3.12.14-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9888e60c2c54eaf56704b17feb558c7ed6b7439bca1e07d4818ab878f2083660", size = 1718649 },
{ url = "https://files.pythonhosted.org/packages/b9/34/8d6015a729f6571341a311061b578e8b8072ea3656b3d72329fa0faa2c7c/aiohttp-3.12.14-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3006a1dc579b9156de01e7916d38c63dc1ea0679b14627a37edf6151bc530088", size = 1634452 },
{ url = "https://files.pythonhosted.org/packages/ff/4b/08b83ea02595a582447aeb0c1986792d0de35fe7a22fb2125d65091cbaf3/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa8ec5c15ab80e5501a26719eb48a55f3c567da45c6ea5bb78c52c036b2655c7", size = 1695511 },
{ url = "https://files.pythonhosted.org/packages/b5/66/9c7c31037a063eec13ecf1976185c65d1394ded4a5120dd5965e3473cb21/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:39b94e50959aa07844c7fe2206b9f75d63cc3ad1c648aaa755aa257f6f2498a9", size = 1716967 },
{ url = "https://files.pythonhosted.org/packages/ba/02/84406e0ad1acb0fb61fd617651ab6de760b2d6a31700904bc0b33bd0894d/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:04c11907492f416dad9885d503fbfc5dcb6768d90cad8639a771922d584609d3", size = 1657620 },
{ url = "https://files.pythonhosted.org/packages/07/53/da018f4013a7a179017b9a274b46b9a12cbeb387570f116964f498a6f211/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:88167bd9ab69bb46cee91bd9761db6dfd45b6e76a0438c7e884c3f8160ff21eb", size = 1737179 },
{ url = "https://files.pythonhosted.org/packages/49/e8/ca01c5ccfeaafb026d85fa4f43ceb23eb80ea9c1385688db0ef322c751e9/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:791504763f25e8f9f251e4688195e8b455f8820274320204f7eafc467e609425", size = 1765156 },
{ url = "https://files.pythonhosted.org/packages/22/32/5501ab525a47ba23c20613e568174d6c63aa09e2caa22cded5c6ea8e3ada/aiohttp-3.12.14-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2785b112346e435dd3a1a67f67713a3fe692d288542f1347ad255683f066d8e0", size = 1724766 },
{ url = "https://files.pythonhosted.org/packages/06/af/28e24574801fcf1657945347ee10df3892311c2829b41232be6089e461e7/aiohttp-3.12.14-cp312-cp312-win32.whl", hash = "sha256:15f5f4792c9c999a31d8decf444e79fcfd98497bf98e94284bf390a7bb8c1729", size = 422641 },
{ url = "https://files.pythonhosted.org/packages/98/d5/7ac2464aebd2eecac38dbe96148c9eb487679c512449ba5215d233755582/aiohttp-3.12.14-cp312-cp312-win_amd64.whl", hash = "sha256:3b66e1a182879f579b105a80d5c4bd448b91a57e8933564bf41665064796a338", size = 449316 },
{ url = "https://files.pythonhosted.org/packages/06/48/e0d2fa8ac778008071e7b79b93ab31ef14ab88804d7ba71b5c964a7c844e/aiohttp-3.12.14-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:3143a7893d94dc82bc409f7308bc10d60285a3cd831a68faf1aa0836c5c3c767", size = 695471 },
{ url = "https://files.pythonhosted.org/packages/8d/e7/f73206afa33100804f790b71092888f47df65fd9a4cd0e6800d7c6826441/aiohttp-3.12.14-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3d62ac3d506cef54b355bd34c2a7c230eb693880001dfcda0bf88b38f5d7af7e", size = 473128 },
{ url = "https://files.pythonhosted.org/packages/df/e2/4dd00180be551a6e7ee979c20fc7c32727f4889ee3fd5b0586e0d47f30e1/aiohttp-3.12.14-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:48e43e075c6a438937c4de48ec30fa8ad8e6dfef122a038847456bfe7b947b63", size = 465426 },
{ url = "https://files.pythonhosted.org/packages/de/dd/525ed198a0bb674a323e93e4d928443a680860802c44fa7922d39436b48b/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:077b4488411a9724cecc436cbc8c133e0d61e694995b8de51aaf351c7578949d", size = 1704252 },
{ url = "https://files.pythonhosted.org/packages/d8/b1/01e542aed560a968f692ab4fc4323286e8bc4daae83348cd63588e4f33e3/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d8c35632575653f297dcbc9546305b2c1133391089ab925a6a3706dfa775ccab", size = 1685514 },
{ url = "https://files.pythonhosted.org/packages/b3/06/93669694dc5fdabdc01338791e70452d60ce21ea0946a878715688d5a191/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6b8ce87963f0035c6834b28f061df90cf525ff7c9b6283a8ac23acee6502afd4", size = 1737586 },
{ url = "https://files.pythonhosted.org/packages/a5/3a/18991048ffc1407ca51efb49ba8bcc1645961f97f563a6c480cdf0286310/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0a2cf66e32a2563bb0766eb24eae7e9a269ac0dc48db0aae90b575dc9583026", size = 1786958 },
{ url = "https://files.pythonhosted.org/packages/30/a8/81e237f89a32029f9b4a805af6dffc378f8459c7b9942712c809ff9e76e5/aiohttp-3.12.14-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdea089caf6d5cde975084a884c72d901e36ef9c2fd972c9f51efbbc64e96fbd", size = 1709287 },
{ url = "https://files.pythonhosted.org/packages/8c/e3/bd67a11b0fe7fc12c6030473afd9e44223d456f500f7cf526dbaa259ae46/aiohttp-3.12.14-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a7865f27db67d49e81d463da64a59365ebd6b826e0e4847aa111056dcb9dc88", size = 1622990 },
{ url = "https://files.pythonhosted.org/packages/83/ba/e0cc8e0f0d9ce0904e3cf2d6fa41904e379e718a013c721b781d53dcbcca/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0ab5b38a6a39781d77713ad930cb5e7feea6f253de656a5f9f281a8f5931b086", size = 1676015 },
{ url = "https://files.pythonhosted.org/packages/d8/b3/1e6c960520bda094c48b56de29a3d978254637ace7168dd97ddc273d0d6c/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b3b15acee5c17e8848d90a4ebc27853f37077ba6aec4d8cb4dbbea56d156933", size = 1707678 },
{ url = "https://files.pythonhosted.org/packages/0a/19/929a3eb8c35b7f9f076a462eaa9830b32c7f27d3395397665caa5e975614/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e4c972b0bdaac167c1e53e16a16101b17c6d0ed7eac178e653a07b9f7fad7151", size = 1650274 },
{ url = "https://files.pythonhosted.org/packages/22/e5/81682a6f20dd1b18ce3d747de8eba11cbef9b270f567426ff7880b096b48/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7442488b0039257a3bdbc55f7209587911f143fca11df9869578db6c26feeeb8", size = 1726408 },
{ url = "https://files.pythonhosted.org/packages/8c/17/884938dffaa4048302985483f77dfce5ac18339aad9b04ad4aaa5e32b028/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f68d3067eecb64c5e9bab4a26aa11bd676f4c70eea9ef6536b0a4e490639add3", size = 1759879 },
{ url = "https://files.pythonhosted.org/packages/95/78/53b081980f50b5cf874359bde707a6eacd6c4be3f5f5c93937e48c9d0025/aiohttp-3.12.14-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f88d3704c8b3d598a08ad17d06006cb1ca52a1182291f04979e305c8be6c9758", size = 1708770 },
{ url = "https://files.pythonhosted.org/packages/ed/91/228eeddb008ecbe3ffa6c77b440597fdf640307162f0c6488e72c5a2d112/aiohttp-3.12.14-cp313-cp313-win32.whl", hash = "sha256:a3c99ab19c7bf375c4ae3debd91ca5d394b98b6089a03231d4c580ef3c2ae4c5", size = 421688 },
{ url = "https://files.pythonhosted.org/packages/66/5f/8427618903343402fdafe2850738f735fd1d9409d2a8f9bcaae5e630d3ba/aiohttp-3.12.14-cp313-cp313-win_amd64.whl", hash = "sha256:3f8aad695e12edc9d571f878c62bedc91adf30c760c8632f09663e5f564f4baa", size = 448098 },
]
[[package]]
@ -264,11 +264,11 @@ wheels = [
[[package]]
name = "certifi"
version = "2025.6.15"
version = "2025.7.14"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753 }
sdist = { url = "https://files.pythonhosted.org/packages/b3/76/52c535bcebe74590f296d6c77c86dabf761c41980e1347a2422e4aa2ae41/certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995", size = 163981 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650 },
{ url = "https://files.pythonhosted.org/packages/4f/52/34c6cf5bb9285074dc3531c437b3919e825d976fde097a7a73f79e726d03/certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2", size = 162722 },
]
[[package]]
@ -456,23 +456,23 @@ wheels = [
[[package]]
name = "debugpy"
version = "1.8.14"
version = "1.8.15"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bd/75/087fe07d40f490a78782ff3b0a30e3968936854105487decdb33446d4b0e/debugpy-1.8.14.tar.gz", hash = "sha256:7cd287184318416850aa8b60ac90105837bb1e59531898c07569d197d2ed5322", size = 1641444 }
sdist = { url = "https://files.pythonhosted.org/packages/8c/8b/3a9a28ddb750a76eaec445c7f4d3147ea2c579a97dbd9e25d39001b92b21/debugpy-1.8.15.tar.gz", hash = "sha256:58d7a20b7773ab5ee6bdfb2e6cf622fdf1e40c9d5aef2857d85391526719ac00", size = 1643279 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/67/e8/57fe0c86915671fd6a3d2d8746e40485fd55e8d9e682388fbb3a3d42b86f/debugpy-1.8.14-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:1b2ac8c13b2645e0b1eaf30e816404990fbdb168e193322be8f545e8c01644a9", size = 2175064 },
{ url = "https://files.pythonhosted.org/packages/3b/97/2b2fd1b1c9569c6764ccdb650a6f752e4ac31be465049563c9eb127a8487/debugpy-1.8.14-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cf431c343a99384ac7eab2f763980724834f933a271e90496944195318c619e2", size = 3132359 },
{ url = "https://files.pythonhosted.org/packages/c0/ee/b825c87ed06256ee2a7ed8bab8fb3bb5851293bf9465409fdffc6261c426/debugpy-1.8.14-cp311-cp311-win32.whl", hash = "sha256:c99295c76161ad8d507b413cd33422d7c542889fbb73035889420ac1fad354f2", size = 5133269 },
{ url = "https://files.pythonhosted.org/packages/d5/a6/6c70cd15afa43d37839d60f324213843174c1d1e6bb616bd89f7c1341bac/debugpy-1.8.14-cp311-cp311-win_amd64.whl", hash = "sha256:7816acea4a46d7e4e50ad8d09d963a680ecc814ae31cdef3622eb05ccacf7b01", size = 5158156 },
{ url = "https://files.pythonhosted.org/packages/d9/2a/ac2df0eda4898f29c46eb6713a5148e6f8b2b389c8ec9e425a4a1d67bf07/debugpy-1.8.14-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:8899c17920d089cfa23e6005ad9f22582fd86f144b23acb9feeda59e84405b84", size = 2501268 },
{ url = "https://files.pythonhosted.org/packages/10/53/0a0cb5d79dd9f7039169f8bf94a144ad3efa52cc519940b3b7dde23bcb89/debugpy-1.8.14-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6bb5c0dcf80ad5dbc7b7d6eac484e2af34bdacdf81df09b6a3e62792b722826", size = 4221077 },
{ url = "https://files.pythonhosted.org/packages/f8/d5/84e01821f362327bf4828728aa31e907a2eca7c78cd7c6ec062780d249f8/debugpy-1.8.14-cp312-cp312-win32.whl", hash = "sha256:281d44d248a0e1791ad0eafdbbd2912ff0de9eec48022a5bfbc332957487ed3f", size = 5255127 },
{ url = "https://files.pythonhosted.org/packages/33/16/1ed929d812c758295cac7f9cf3dab5c73439c83d9091f2d91871e648093e/debugpy-1.8.14-cp312-cp312-win_amd64.whl", hash = "sha256:5aa56ef8538893e4502a7d79047fe39b1dae08d9ae257074c6464a7b290b806f", size = 5297249 },
{ url = "https://files.pythonhosted.org/packages/4d/e4/395c792b243f2367d84202dc33689aa3d910fb9826a7491ba20fc9e261f5/debugpy-1.8.14-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:329a15d0660ee09fec6786acdb6e0443d595f64f5d096fc3e3ccf09a4259033f", size = 2485676 },
{ url = "https://files.pythonhosted.org/packages/ba/f1/6f2ee3f991327ad9e4c2f8b82611a467052a0fb0e247390192580e89f7ff/debugpy-1.8.14-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f920c7f9af409d90f5fd26e313e119d908b0dd2952c2393cd3247a462331f15", size = 4217514 },
{ url = "https://files.pythonhosted.org/packages/79/28/b9d146f8f2dc535c236ee09ad3e5ac899adb39d7a19b49f03ac95d216beb/debugpy-1.8.14-cp313-cp313-win32.whl", hash = "sha256:3784ec6e8600c66cbdd4ca2726c72d8ca781e94bce2f396cc606d458146f8f4e", size = 5254756 },
{ url = "https://files.pythonhosted.org/packages/e0/62/a7b4a57013eac4ccaef6977966e6bec5c63906dd25a86e35f155952e29a1/debugpy-1.8.14-cp313-cp313-win_amd64.whl", hash = "sha256:684eaf43c95a3ec39a96f1f5195a7ff3d4144e4a18d69bb66beeb1a6de605d6e", size = 5297119 },
{ url = "https://files.pythonhosted.org/packages/97/1a/481f33c37ee3ac8040d3d51fc4c4e4e7e61cb08b8bc8971d6032acc2279f/debugpy-1.8.14-py2.py3-none-any.whl", hash = "sha256:5cd9a579d553b6cb9759a7908a41988ee6280b961f24f63336835d9418216a20", size = 5256230 },
{ url = "https://files.pythonhosted.org/packages/d2/b3/1c44a2ed311199ab11c2299c9474a6c7cd80d19278defd333aeb7c287995/debugpy-1.8.15-cp311-cp311-macosx_14_0_universal2.whl", hash = "sha256:babc4fb1962dd6a37e94d611280e3d0d11a1f5e6c72ac9b3d87a08212c4b6dd3", size = 2183442 },
{ url = "https://files.pythonhosted.org/packages/f6/69/e2dcb721491e1c294d348681227c9b44fb95218f379aa88e12a19d85528d/debugpy-1.8.15-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f778e68f2986a58479d0ac4f643e0b8c82fdd97c2e200d4d61e7c2d13838eb53", size = 3134215 },
{ url = "https://files.pythonhosted.org/packages/17/76/4ce63b95d8294dcf2fd1820860b300a420d077df4e93afcaa25a984c2ca7/debugpy-1.8.15-cp311-cp311-win32.whl", hash = "sha256:f9d1b5abd75cd965e2deabb1a06b0e93a1546f31f9f621d2705e78104377c702", size = 5154037 },
{ url = "https://files.pythonhosted.org/packages/c2/a7/e5a7c784465eb9c976d84408873d597dc7ce74a0fc69ed009548a1a94813/debugpy-1.8.15-cp311-cp311-win_amd64.whl", hash = "sha256:62954fb904bec463e2b5a415777f6d1926c97febb08ef1694da0e5d1463c5c3b", size = 5178133 },
{ url = "https://files.pythonhosted.org/packages/ab/4a/4508d256e52897f5cdfee6a6d7580974811e911c6d01321df3264508a5ac/debugpy-1.8.15-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:3dcc7225cb317469721ab5136cda9ff9c8b6e6fb43e87c9e15d5b108b99d01ba", size = 2511197 },
{ url = "https://files.pythonhosted.org/packages/99/8d/7f6ef1097e7fecf26b4ef72338d08e41644a41b7ee958a19f494ffcffc29/debugpy-1.8.15-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:047a493ca93c85ccede1dbbaf4e66816794bdc214213dde41a9a61e42d27f8fc", size = 4229517 },
{ url = "https://files.pythonhosted.org/packages/3f/e8/e8c6a9aa33a9c9c6dacbf31747384f6ed2adde4de2e9693c766bdf323aa3/debugpy-1.8.15-cp312-cp312-win32.whl", hash = "sha256:b08e9b0bc260cf324c890626961dad4ffd973f7568fbf57feb3c3a65ab6b6327", size = 5276132 },
{ url = "https://files.pythonhosted.org/packages/e9/ad/231050c6177b3476b85fcea01e565dac83607b5233d003ff067e2ee44d8f/debugpy-1.8.15-cp312-cp312-win_amd64.whl", hash = "sha256:e2a4fe357c92334272eb2845fcfcdbec3ef9f22c16cf613c388ac0887aed15fa", size = 5317645 },
{ url = "https://files.pythonhosted.org/packages/28/70/2928aad2310726d5920b18ed9f54b9f06df5aa4c10cf9b45fa18ff0ab7e8/debugpy-1.8.15-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:f5e01291ad7d6649aed5773256c5bba7a1a556196300232de1474c3c372592bf", size = 2495538 },
{ url = "https://files.pythonhosted.org/packages/9e/c6/9b8ffb4ca91fac8b2877eef63c9cc0e87dd2570b1120054c272815ec4cd0/debugpy-1.8.15-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94dc0f0d00e528d915e0ce1c78e771475b2335b376c49afcc7382ee0b146bab6", size = 4221874 },
{ url = "https://files.pythonhosted.org/packages/55/8a/9b8d59674b4bf489318c7c46a1aab58e606e583651438084b7e029bf3c43/debugpy-1.8.15-cp313-cp313-win32.whl", hash = "sha256:fcf0748d4f6e25f89dc5e013d1129ca6f26ad4da405e0723a4f704583896a709", size = 5275949 },
{ url = "https://files.pythonhosted.org/packages/72/83/9e58e6fdfa8710a5e6ec06c2401241b9ad48b71c0a7eb99570a1f1edb1d3/debugpy-1.8.15-cp313-cp313-win_amd64.whl", hash = "sha256:73c943776cb83e36baf95e8f7f8da765896fd94b05991e7bc162456d25500683", size = 5317720 },
{ url = "https://files.pythonhosted.org/packages/07/d5/98748d9860e767a1248b5e31ffa7ce8cb7006e97bf8abbf3d891d0a8ba4e/debugpy-1.8.15-py2.py3-none-any.whl", hash = "sha256:bce2e6c5ff4f2e00b98d45e7e01a49c7b489ff6df5f12d881c67d2f1ac635f3d", size = 5282697 },
]
[[package]]
@ -906,15 +906,15 @@ wheels = [
[[package]]
name = "pyinstaller-hooks-contrib"
version = "2025.5"
version = "2025.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "packaging" },
{ name = "setuptools" },
]
sdist = { url = "https://files.pythonhosted.org/packages/5f/ff/e3376595935d5f8135964d2177cd3e3e0c1b5a6237497d9775237c247a5d/pyinstaller_hooks_contrib-2025.5.tar.gz", hash = "sha256:707386770b8fe066c04aad18a71bc483c7b25e18b4750a756999f7da2ab31982", size = 163124 }
sdist = { url = "https://files.pythonhosted.org/packages/ea/f7/d375cf8112d839afaf05b90c70dd128624473fd915fce211f5646b0afbc7/pyinstaller_hooks_contrib-2025.6.tar.gz", hash = "sha256:223ae773733fb7a0ee9cb5e817480998a90a6c7a9c3d2b7b580d2dfa2b325751", size = 163799 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/2c/b4d317534e17dd1df95c394d4b37febb15ead006a1c07c2bb006481fb5e7/pyinstaller_hooks_contrib-2025.5-py3-none-any.whl", hash = "sha256:ebfae1ba341cb0002fb2770fad0edf2b3e913c2728d92df7ad562260988ca373", size = 437246 },
{ url = "https://files.pythonhosted.org/packages/da/e6/ab065bd226099a4e39aa08a54810f846beb7a9c534fa221ee750a3befa25/pyinstaller_hooks_contrib-2025.6-py3-none-any.whl", hash = "sha256:06779d024f7d60dd75b05520923bba16b17df5f64073434b23e570ffb71094dc", size = 440590 },
]
[[package]]