diff --git a/.vscode/settings.json b/.vscode/settings.json
index e2d04f93..2439e05e 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -16,6 +16,8 @@
"ahas",
"ahash",
"aiocron",
+ "aiosqlite",
+ "alnum",
"anyio",
"Archiver",
"arrowdown",
@@ -73,6 +75,8 @@
"falsey",
"faststart",
"Fetc",
+ "fetchall",
+ "fetchone",
"filterwarnings",
"finaldir",
"flac",
@@ -91,6 +95,7 @@
"hookspath",
"httpx",
"imagetools",
+ "isinstance",
"jmespath",
"jsonschema",
"kibibytes",
@@ -112,7 +117,10 @@
"mebibytes",
"MEIPASS",
"merch",
+ "metaclass",
+ "Metaclasses",
"metadataparser",
+ "mgmt",
"Microformat",
"microformats",
"mkvtoolsnix",
@@ -128,6 +136,7 @@
"noninteractive",
"noprogress",
"onefile",
+ "oneline",
"parsel",
"pathex",
"pickleable",
@@ -178,6 +187,7 @@
"Unraid",
"upgrader",
"urandom",
+ "urlparse",
"urlsafe",
"usegmt",
"usermod",
@@ -190,6 +200,7 @@
"xerror",
"youtu",
"youtubepot",
+ "zstd",
"русский",
"العربية"
],
diff --git a/app/library/BackgroundWorker.py b/app/library/BackgroundWorker.py
index 48add1d3..f5f4dea9 100644
--- a/app/library/BackgroundWorker.py
+++ b/app/library/BackgroundWorker.py
@@ -6,6 +6,7 @@ from queue import Empty, Queue
from aiohttp import web
+from .Services import Services
from .Singleton import Singleton
LOG: logging.Logger = logging.getLogger("BackgroundWorker")
@@ -35,6 +36,7 @@ class BackgroundWorker(metaclass=Singleton):
return BackgroundWorker()
def attach(self, app: web.Application):
+ Services.get_instance().add("background_worker", self)
app.on_shutdown.append(self.on_shutdown)
LOG.debug("Starting background worker...")
diff --git a/app/library/DataStore.py b/app/library/DataStore.py
index 7d668189..33cb44d5 100644
--- a/app/library/DataStore.py
+++ b/app/library/DataStore.py
@@ -1,27 +1,20 @@
import copy
-import json
import logging
from collections import OrderedDict
-from dataclasses import fields
-from datetime import UTC, datetime
-from email.utils import formatdate
+from collections.abc import Iterable
from enum import Enum
-from sqlite3 import Connection
from .Download import Download
from .ItemDTO import ItemDTO
from .operations import matches_condition
-from .Utils import init_class
+from .sqlite_store import SqliteStore
-LOG: logging.Logger = logging.getLogger("datastore")
-
-ITEM_DTO_FIELDS = {f.name for f in fields(ItemDTO)}
-"""Caching ItemDTO fields for performance."""
+LOG = logging.getLogger("datastore")
class StoreType(str, Enum):
- HISTORY: str = "done"
- QUEUE: str = "queue"
+ HISTORY = "done"
+ QUEUE = "queue"
@classmethod
def all(cls) -> list[str]:
@@ -29,48 +22,46 @@ class StoreType(str, Enum):
@classmethod
def from_value(cls, value: str) -> "StoreType":
- """
- Returns the StoreType enum member corresponding to the given value.
-
- Args:
- value (str): The value to match against the enum members.
-
- Returns:
- StoreType: The enum member that matches the value.
-
- Raises:
- ValueError: If the value does not match any member.
-
- """
for member in cls:
if member.value == value:
return member
-
msg = f"Invalid StoreType value: {value}"
raise ValueError(msg)
- def __str__(self) -> str:
+ def __str__(self) -> str: # pragma: no cover
return self.value
+def _strip_transient_fields(item: ItemDTO) -> ItemDTO:
+ stored = copy.deepcopy(item)
+ if hasattr(stored, "datetime"):
+ try:
+ delattr(stored, "datetime")
+ except AttributeError:
+ pass
+ if stored.status == "finished" and hasattr(stored, "live_in"):
+ try:
+ delattr(stored, "live_in")
+ except AttributeError:
+ pass
+ return stored
+
+
class DataStore:
- """
- Persistent queue.
- """
-
- def __init__(self, type: StoreType, connection: Connection):
+ def __init__(self, type: StoreType, connection: SqliteStore):
+ self._type = type
+ self._connection: SqliteStore = connection
self._dict: OrderedDict[str, Download] = OrderedDict()
- "The dictionary of items."
- self._type: StoreType = type
- "The type of the datastore."
- self._connection: Connection = connection
- "The database connection."
- def load(self) -> None:
- for id, item in self.saved_items():
- self._dict.update({id: Download(info=item)})
+ async def load(self) -> None:
+ saved = await self._connection.fetch_saved(str(self._type))
+ for key, item in saved:
+ self._dict[key] = Download(info=item)
- def exists(self, key: str | None = None, url: str | None = None) -> bool:
+ async def saved_items(self) -> list[tuple[str, ItemDTO]]:
+ return await self._connection.fetch_saved(str(self._type))
+
+ async def exists(self, key: str | None = None, url: str | None = None) -> bool:
if not key and not url:
msg = "key or url must be provided."
raise KeyError(msg)
@@ -78,11 +69,12 @@ class DataStore:
if key and key in self._dict:
return True
- return any(
- (key and self._dict[i].info._id == key) or (url and self._dict[i].info.url == url) for i in self._dict
- )
+ if any((key and self._dict[i].info._id == key) or (url and self._dict[i].info.url == url) for i in self._dict):
+ return True
- def get(self, key: str | None = None, url: str | None = None) -> Download:
+ return StoreType.HISTORY == self._type and await self._connection.exists(str(self._type), key=key, url=url)
+
+ async def get(self, key: str | None = None, url: str | None = None) -> Download:
if not key and not url:
msg = "key or url must be provided."
raise KeyError(msg)
@@ -91,37 +83,14 @@ class DataStore:
if (key and self._dict[i].info._id == key) or (url and self._dict[i].info.url == url):
return self._dict[i]
+ if StoreType.HISTORY == self._type and (item := await self._connection.get(str(self._type), key=key, url=url)):
+ self._dict[item._id] = Download(info=item)
+ return self._dict[item._id]
+
msg: str = f"{key=} or {url=} not found."
raise KeyError(msg)
- def get_item(self, **kwargs) -> Download | None:
- """
- Get a specific item from the datastore based on provided attributes with optional operations.
-
- Args:
- **kwargs: Arbitrary keyword arguments representing attributes of the ItemDTO.
- Each value can be either:
- - A direct value (defaults to EQUAL operation): {"title": "test"}
- - A tuple of (Operation, value): {"title": (Operation.CONTAIN, "test")}
-
- If no attributes are provided, the method returns None.
- If any attribute matches, the corresponding Download object is returned.
-
- Returns:
- Download | None: The requested item if found, otherwise None.
-
- Examples:
- # Direct equality check (default)
- store.get_item(title="Video 1")
-
- # Using operations
- store.get_item(title=(Operation.CONTAIN, "test"))
- store.get_item(id=(Operation.EQUAL, "123"), status=(Operation.NOT_EQUAL, "error"))
-
- # Mixed usage
- store.get_item(title=(Operation.CONTAIN, "test"), folder="downloads")
-
- """
+ async def get_item(self, **kwargs) -> Download | None:
if not kwargs:
return None
@@ -130,118 +99,62 @@ class DataStore:
continue
info = self._dict[i].info.__dict__
+
if any(matches_condition(key, value, info) for key, value in kwargs.items()):
return self._dict[i]
+ if StoreType.HISTORY == self._type and (item := await self._connection.get_item(str(self._type), **kwargs)):
+ self._dict[item._id] = Download(info=item)
+ return self._dict[item._id]
+
return None
- def get_by_id(self, id: str) -> Download | None:
- return self._dict.get(id, None)
+ async def get_by_id(self, id: str) -> Download | None:
+ val = self._dict.get(id)
+ if val or self._type == StoreType.QUEUE:
+ return val
- def items(self) -> OrderedDict[tuple[str, Download]]:
+ if item := await self._connection.get_by_id(str(self._type), id):
+ self._dict[item._id] = Download(info=item)
+ return self._dict[id]
+
+ return None
+
+ def items(self):
return self._dict.items()
- def saved_items(self) -> list[tuple[str, ItemDTO]]:
- items: list[tuple[str, ItemDTO]] = []
-
- cursor = self._connection.execute(
- 'SELECT "id", "data", "created_at" FROM "history" WHERE "type" = ? ORDER BY "created_at" ASC',
- (str(self._type),),
- )
-
- for row in cursor:
- rowDate: datetime = datetime.strptime(row["created_at"], "%Y-%m-%d %H:%M:%S") # noqa: DTZ007
- data: dict = json.loads(row["data"])
- data.pop("_id", None)
- item: ItemDTO = init_class(ItemDTO, data, ITEM_DTO_FIELDS)
- item._id = row["id"]
- item.datetime = formatdate(rowDate.replace(tzinfo=UTC).timestamp())
- items.append((row["id"], item))
-
- return items
-
- def put(self, value: Download, no_notify: bool = False) -> Download:
- if "error" == value.info.status and not no_notify:
- from app.library.Events import EventBus, Events
-
- EventBus.get_instance().emit(Events.ITEM_ERROR, value.info)
-
- self._dict.update({value.info._id: value})
- self._update_store_item(self._type, value.info)
-
+ async def put(self, value: Download, no_notify: bool = False) -> Download:
+ _ = no_notify
+ self._dict[value.info._id] = value
+ await self._connection.enqueue_upsert(str(self._type), _strip_transient_fields(value.info))
return self._dict[value.info._id]
- def delete(self, key: str) -> None:
+ async def delete(self, key: str) -> None:
self._dict.pop(key, None)
- self._delete_store_item(key)
+ await self._connection.enqueue_delete(str(self._type), key)
- def next(self) -> tuple[str, Download]:
+ def next(self):
return next(iter(self._dict.items()))
- def empty(self):
- return not bool(self._dict)
-
- def has_downloads(self):
- if 0 == len(self._dict):
- return False
+ def empty(self) -> bool:
+ return 0 == len(self._dict)
+ def has_downloads(self) -> bool:
return any(self._dict[key].info.auto_start and self._dict[key].started() is False for key in self._dict)
- def get_next_download(self) -> Download:
+ def get_next_download(self) -> Download | None:
for key in self._dict:
ref = self._dict[key]
- if ref.info.auto_start and ref.started() is False and ref.is_cancelled() is False:
+ if ref.info.auto_start and not ref.started() and not ref.is_cancelled():
return ref
-
return None
- async def test(self) -> bool:
- self._connection.execute('SELECT "id" FROM "history" LIMIT 1').fetchone()
- return True
+ async def get_total_count(self, status_filter: str | None = None) -> int:
+ return await self._connection.count(str(self._type), status_filter=status_filter)
- def get_total_count(self) -> int:
- """
- Get the total count of items in the datastore.
-
- Returns:
- int: The total number of items in the datastore.
-
- """
- cursor = self._connection.execute(
- 'SELECT COUNT(*) as count FROM "history" WHERE "type" = ?',
- (str(self._type),),
- )
- row = cursor.fetchone()
- return row["count"] if row else 0
-
- def get_items_paginated(
- self,
- page: int = 1,
- per_page: int = 50,
- order: str = "DESC",
- status_filter: str | None = None,
- ) -> tuple[list[tuple[str, ItemDTO]], int, int, int]:
- """
- Get paginated items from the datastore.
-
- Args:
- page (int): The page number (1-indexed). Defaults to 1.
- per_page (int): Number of items per page. Defaults to 50.
- order (str): Sort order - 'ASC' or 'DESC'. Defaults to 'DESC' (newest first).
- status_filter (str | None): Optional status filter. Can be a status value (e.g., 'finished')
- to include only that status, or prefixed with '!' (e.g., '!finished') to exclude that status.
-
- Returns:
- tuple[list[tuple[str, ItemDTO]], int, int, int]: A tuple containing:
- - List of (id, ItemDTO) tuples for the requested page
- - Total number of items
- - Current page number
- - Total number of pages
-
- Raises:
- ValueError: If page < 1 or per_page < 1
-
- """
+ async def get_items_paginated(
+ self, page: int = 1, per_page: int = 50, order: str = "DESC", status_filter: str | None = None
+ ) -> tuple[list[tuple[str, Download]], int, int, int]:
if page < 1:
msg = "page must be >= 1"
raise ValueError(msg)
@@ -255,92 +168,18 @@ class DataStore:
msg = f"order must be 'ASC' or 'DESC', got '{order}'"
raise ValueError(msg)
- order = "ASC" if order == "ASC" else "DESC"
-
- # Build SQL query with status filter if provided
- where_clauses = ['"type" = ?']
- query_params: list = [str(self._type)]
-
- if status_filter:
- # Check if it's an exclusion filter (starts with !)
- if status_filter.startswith("!"):
- status_value = status_filter[1:]
- where_clauses.append("json_extract(data, '$.status') != ?")
- query_params.append(status_value)
- else:
- where_clauses.append("json_extract(data, '$.status') = ?")
- query_params.append(status_filter)
-
- where_clause = " AND ".join(where_clauses)
-
- # Get total count with filter
- count_query = f'SELECT COUNT(*) as count FROM "history" WHERE {where_clause}' # noqa: S608
- count_result = self._connection.execute(count_query, tuple(query_params)).fetchone()
- total_items: int = count_result["count"] if count_result else 0
- total_pages: int = (total_items + per_page - 1) // per_page if total_items > 0 else 1
-
- # Ensure page is within valid range.
- if page > total_pages and total_items > 0:
- page = total_pages
-
- offset = (page - 1) * per_page
-
- items: list[tuple[str, ItemDTO]] = []
-
- query_params.extend([per_page, offset])
-
- cursor = self._connection.execute(
- f'SELECT "id", "data", "created_at" FROM "history" WHERE {where_clause} ORDER BY "created_at" {order} LIMIT ? OFFSET ?', # noqa: S608
- tuple(query_params),
+ items, total_items, current_page, total_pages = await self._connection.paginate(
+ str(self._type), page, per_page, order, status_filter
)
- for row in cursor:
- rowDate: datetime = datetime.strptime(row["created_at"], "%Y-%m-%d %H:%M:%S") # noqa: DTZ007
- data: dict = json.loads(row["data"])
- data.pop("_id", None)
- item: ItemDTO = init_class(ItemDTO, data, ITEM_DTO_FIELDS)
- item._id = row["id"]
- item.datetime = formatdate(rowDate.replace(tzinfo=UTC).timestamp())
- items.append((row["id"], item))
+ return [(item_id, Download(info=item)) for item_id, item in items], total_items, current_page, total_pages
- return items, total_items, page, total_pages
+ async def bulk_delete(self, ids: Iterable[str]) -> int:
+ deleted = await self._connection.bulk_delete(str(self._type), ids)
+ for _id in ids:
+ self._dict.pop(_id, None)
+ return deleted
- def _update_store_item(self, type: StoreType, item: ItemDTO) -> None:
- sqlStatement = """
- INSERT INTO "history" ("id", "type", "url", "data")
- VALUES (?, ?, ?, ?)
- ON CONFLICT DO UPDATE SET "type" = ?, "url" = ?, "data" = ?, created_at = ?
- """
-
- stored: ItemDTO = copy.deepcopy(item)
-
- if hasattr(stored, "datetime"):
- try:
- delattr(stored, "datetime")
- except AttributeError:
- pass
-
- if hasattr(stored, "live_in") and stored.status == "finished":
- try:
- delattr(stored, "live_in")
- except AttributeError:
- pass
-
- encoded: str = stored.json()
-
- self._connection.execute(
- sqlStatement.strip(),
- (
- stored._id,
- str(type),
- stored.url,
- encoded,
- str(type),
- stored.url,
- encoded,
- datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S"),
- ),
- )
-
- def _delete_store_item(self, key: str) -> None:
- self._connection.execute('DELETE FROM "history" WHERE "type" = ? AND "id" = ?', (str(self._type), key))
+ async def test(self) -> bool:
+ await self._connection.count(str(self._type))
+ return True
diff --git a/app/library/DownloadQueue.py b/app/library/DownloadQueue.py
index 544415f6..5a1d4fbd 100644
--- a/app/library/DownloadQueue.py
+++ b/app/library/DownloadQueue.py
@@ -9,7 +9,6 @@ import uuid
from datetime import UTC, datetime, timedelta
from email.utils import formatdate
from pathlib import Path
-from sqlite3 import Connection
from typing import TYPE_CHECKING, Any
import yt_dlp.utils
@@ -24,7 +23,9 @@ from .Events import EventBus, Events
from .ItemDTO import Item, ItemDTO
from .Presets import Presets
from .Scheduler import Scheduler
+from .Services import Services
from .Singleton import Singleton
+from .sqlite_store import SqliteStore
from .Utils import (
archive_add,
arg_converter,
@@ -49,14 +50,14 @@ class DownloadQueue(metaclass=Singleton):
DownloadQueue class is a singleton class that manages the download queue and the download history.
"""
- def __init__(self, connection: Connection, config: Config | None = None):
+ def __init__(self, config: Config | None = None):
self.config: Config = config or Config.get_instance()
"Configuration instance."
self._notify: EventBus = EventBus.get_instance()
"Event bus instance."
- self.done = DataStore(type=StoreType.HISTORY, connection=connection)
+ self.done = DataStore(type=StoreType.HISTORY, connection=SqliteStore.get_instance())
"DataStore for the completed downloads."
- self.queue = DataStore(type=StoreType.QUEUE, connection=connection)
+ self.queue = DataStore(type=StoreType.QUEUE, connection=SqliteStore.get_instance())
"DataStore for the download queue."
self.workers = asyncio.Semaphore(self.config.max_workers)
"Semaphore to limit the number of concurrent downloads."
@@ -71,12 +72,10 @@ class DownloadQueue(metaclass=Singleton):
self._active: dict[str, Download] = {}
"""Dictionary of active downloads."""
- self.done.load()
- self.queue.load()
self.paused.set()
@staticmethod
- def get_instance() -> "DownloadQueue":
+ def get_instance(config: Config | None = None) -> "DownloadQueue":
"""
Get the instance of the DownloadQueue.
@@ -84,7 +83,7 @@ class DownloadQueue(metaclass=Singleton):
DownloadQueue: The instance of the DownloadQueue
"""
- return DownloadQueue()
+ return DownloadQueue(config=config)
def _get_limit(self, extractor: str) -> asyncio.Semaphore:
"""
@@ -123,6 +122,7 @@ class DownloadQueue(metaclass=Singleton):
_ (web.Application): The application to attach the download queue to.
"""
+ Services.get_instance().add("queue", self)
async def event_handler(_, __):
await self.initialize()
@@ -166,6 +166,7 @@ class DownloadQueue(metaclass=Singleton):
"""
Initialize the download queue.
"""
+ await self.queue.load()
LOG.info(
f"Using '{self.config.max_workers}' workers for downloading and '{self.config.max_workers_per_extractor}' per extractor."
)
@@ -187,7 +188,7 @@ class DownloadQueue(metaclass=Singleton):
for item_id in ids:
try:
- item: Download = self.queue.get(key=item_id)
+ item: Download = await self.queue.get(key=item_id)
except KeyError as e:
status[item_id] = f"not found: {e!s}"
status["status"] = "error"
@@ -199,7 +200,7 @@ class DownloadQueue(metaclass=Singleton):
continue
item.info.auto_start = True
- updated: Download = self.queue.put(item)
+ updated: Download = await self.queue.put(item)
self._notify.emit(Events.ITEM_UPDATED, data=updated.info)
self._notify.emit(
Events.ITEM_RESUMED,
@@ -230,7 +231,7 @@ class DownloadQueue(metaclass=Singleton):
for item_id in ids:
try:
- item: Download = self.queue.get(key=item_id)
+ item: Download = await self.queue.get(key=item_id)
except KeyError as e:
status[item_id] = f"not found: {e!s}"
status["status"] = "error"
@@ -246,7 +247,7 @@ class DownloadQueue(metaclass=Singleton):
continue
item.info.auto_start = False
- updated: Download = self.queue.put(item)
+ updated: Download = await self.queue.put(item)
self._notify.emit(Events.ITEM_UPDATED, data=updated.info)
self._notify.emit(
Events.ITEM_PAUSED,
@@ -443,7 +444,7 @@ class DownloadQueue(metaclass=Singleton):
LOG.debug(f"Entry id '{entry.get('id')}' url '{entry.get('webpage_url')} - {entry.get('url')}'.")
try:
- _item: Download = self.done.get(key=entry.get("id"), url=entry.get("webpage_url") or entry.get("url"))
+ _item: Download = await self.done.get(key=entry.get("id"), url=entry.get("webpage_url") or entry.get("url"))
err_msg: str = f"Removing {_item.info.name()} from history list."
LOG.warning(err_msg)
await self.clear([_item.info._id], remove_file=False)
@@ -451,7 +452,7 @@ class DownloadQueue(metaclass=Singleton):
pass
try:
- _item: Download = self.queue.get(
+ _item: Download = await self.queue.get(
key=str(entry.get("id")), url=str(entry.get("webpage_url") or entry.get("url"))
)
err_msg: str = f"Item {_item.info.name()} is already in download queue."
@@ -534,7 +535,7 @@ class DownloadQueue(metaclass=Singleton):
message=nMessage,
)
- itemDownload: Download = self.done.put(dlInfo)
+ itemDownload: Download = await self.done.put(dlInfo)
elif not hasFormats:
ava: str = entry.get("availability", "public")
nTitle = "Download Error"
@@ -547,7 +548,7 @@ class DownloadQueue(metaclass=Singleton):
dlInfo.info.error = nMessage.replace(f" for '{dl.title}'.", ".") + text_logs
dlInfo.info.status = "error"
- itemDownload = self.done.put(dlInfo)
+ itemDownload = await self.done.put(dlInfo)
self._notify.emit(
Events.LOG_WARNING,
@@ -580,12 +581,12 @@ class DownloadQueue(metaclass=Singleton):
if _requeue:
nEvent = Events.ITEM_ADDED
- itemDownload = self.queue.put(dlInfo)
+ itemDownload = await self.queue.put(dlInfo)
if item.auto_start:
self.event.set()
else:
dlInfo.info.status = "not_live"
- itemDownload = self.done.put(dlInfo)
+ itemDownload = await self.done.put(dlInfo)
nStore = "history"
nEvent = Events.ITEM_MOVED
nTitle = "Premiering right now"
@@ -596,7 +597,7 @@ class DownloadQueue(metaclass=Singleton):
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)
+ itemDownload = await self.queue.put(dlInfo)
if item.auto_start:
self.event.set()
else:
@@ -712,7 +713,7 @@ class DownloadQueue(metaclass=Singleton):
if item.is_archived():
if archive_id:
- store_type, _ = self.get_item(archive_id=archive_id)
+ store_type, _ = await self.get_item(archive_id=archive_id)
if not store_type:
dlInfo = Download(
info=ItemDTO(
@@ -732,7 +733,7 @@ class DownloadQueue(metaclass=Singleton):
if archive_file := dlInfo.info.get_ytdlp_opts().get_all().get("download_archive"):
dlInfo.info.msg += f" Found in archive '{archive_file}'."
- self.done.put(dlInfo)
+ await self.done.put(dlInfo)
self._notify.emit(
Events.ITEM_MOVED,
@@ -800,7 +801,7 @@ class DownloadQueue(metaclass=Singleton):
log_message = f"Ignoring download of '{item_title}' as per condition '{condition.name}'{extra_msg}."
- store_type, _ = self.get_item(archive_id=archive_id)
+ store_type, _ = await self.get_item(archive_id=archive_id)
if not store_type:
dlInfo = Download(
info=ItemDTO(
@@ -816,7 +817,7 @@ class DownloadQueue(metaclass=Singleton):
extras=item.extras,
)
)
- self.done.put(dlInfo)
+ await self.done.put(dlInfo)
LOG.info(log_message)
self._notify.emit(Events.LOG_INFO, data={}, title="Ignored Download", message=log_message)
@@ -885,7 +886,7 @@ class DownloadQueue(metaclass=Singleton):
for id in ids:
try:
- item = self.queue.get(key=id)
+ item = await self.queue.get(key=id)
except KeyError as e:
status[id] = str(e)
status["status"] = "error"
@@ -902,7 +903,7 @@ class DownloadQueue(metaclass=Singleton):
else:
await item.close()
LOG.debug(f"Deleting from queue {item_ref}")
- self.queue.delete(id)
+ await self.queue.delete(id)
self._notify.emit(
Events.ITEM_CANCELLED,
data=item.info,
@@ -910,7 +911,7 @@ class DownloadQueue(metaclass=Singleton):
message=f"Download '{item.info.title}' has been cancelled.",
)
item.info.status = "cancelled"
- self.done.put(item)
+ await self.done.put(item)
self._notify.emit(
Events.ITEM_MOVED,
data={"to": "history", "preset": item.info.preset, "item": item.info},
@@ -939,7 +940,7 @@ class DownloadQueue(metaclass=Singleton):
for id in ids:
try:
- item: Download = self.done.get(key=id)
+ item: Download = await self.done.get(key=id)
except KeyError as e:
status[id] = str(e)
status["status"] = "error"
@@ -984,7 +985,7 @@ class DownloadQueue(metaclass=Singleton):
except Exception as e:
LOG.error(f"Unable to remove '{itemRef}' local file '{filename}'. {e!s}")
- self.done.delete(id)
+ await self.done.delete(id)
_status: str = "Removed" if removed_files > 0 else "Cleared"
self._notify.emit(
@@ -1003,7 +1004,7 @@ class DownloadQueue(metaclass=Singleton):
return status
- def get(self, mode: str = "all") -> dict[str, list[dict[str, ItemDTO]]]:
+ async def get(self, mode: str = "all") -> dict[str, list[dict[str, ItemDTO]]]:
"""
Get the download queue and the download history.
@@ -1017,12 +1018,11 @@ class DownloadQueue(metaclass=Singleton):
items = {"queue": {}, "done": {}}
if mode in ("all", "queue"):
- for k, v in self.queue.saved_items():
+ for k, v in await self.queue.saved_items():
items["queue"][k] = self._active[k].info if k in self._active else v
if mode in ("all", "done"):
- for k, v in self.done.saved_items():
- v.get_file_sidecar()
+ for k, v in await self.done.saved_items():
items["done"][k] = v
if mode in ("all", "queue"):
@@ -1035,12 +1035,11 @@ class DownloadQueue(metaclass=Singleton):
if k in items["done"]:
continue
- v.info.get_file_sidecar()
items["done"][k] = v.info
return items
- def get_item(self, **kwargs) -> tuple[StoreType, Download] | tuple[None, None]:
+ async def get_item(self, **kwargs) -> tuple[StoreType, Download] | tuple[None, None]:
"""
Get a specific item from the download queue or history.
@@ -1051,10 +1050,10 @@ class DownloadQueue(metaclass=Singleton):
(StoreType, Download) | None: The requested item if found, otherwise None.
"""
- if item := self.queue.get_item(**kwargs):
+ if item := await self.queue.get_item(**kwargs):
return (StoreType.QUEUE, item)
- if item := self.done.get_item(**kwargs):
+ if item := await self.done.get_item(**kwargs):
return (StoreType.HISTORY, item)
return (None, None)
@@ -1165,9 +1164,9 @@ class DownloadQueue(metaclass=Singleton):
await entry.close()
- if self.queue.exists(key=id):
+ if await self.queue.exists(key=id):
LOG.debug(f"Download Task '{id}' is completed. Removing from queue.")
- self.queue.delete(key=id)
+ await self.queue.delete(key=id)
nTitle: str | None = None
nMessage: str | None = None
@@ -1186,7 +1185,7 @@ class DownloadQueue(metaclass=Singleton):
self._notify.emit(Events.ITEM_COMPLETED, data=entry.info, title=nTitle, message=nMessage)
- self.done.put(entry)
+ await self.done.put(entry)
self._notify.emit(
Events.ITEM_MOVED,
data={"to": "history", "preset": entry.info.preset, "item": entry.info},
@@ -1288,7 +1287,7 @@ class DownloadQueue(metaclass=Singleton):
)
)
except Exception as e:
- self.done.put(item)
+ await self.done.put(item)
LOG.exception(e)
LOG.error(f"Failed to retry item '{item_ref}'. {e!s}")
@@ -1329,7 +1328,7 @@ class DownloadQueue(metaclass=Singleton):
message=f"'{item_name}' record removed from history.",
)
titles.append(item_name)
- self.done.delete(key)
+ await self.done.delete(key)
if titles:
LOG.info(f"Automatically cleared '{', '.join(titles)}' from download history due to age.")
diff --git a/app/library/Events.py b/app/library/Events.py
index f7528927..39a0232a 100644
--- a/app/library/Events.py
+++ b/app/library/Events.py
@@ -25,6 +25,7 @@ class Events:
CONNECTED: str = "connected"
CONFIGURATION: str = "configuration"
+ ACTIVE_QUEUE: str = "active_queue"
LOG_INFO: str = "log_info"
LOG_WARNING: str = "log_warning"
@@ -94,6 +95,7 @@ class Events:
return [
Events.CONFIGURATION,
Events.CONNECTED,
+ Events.ACTIVE_QUEUE,
Events.LOG_INFO,
Events.LOG_WARNING,
Events.LOG_ERROR,
diff --git a/app/library/HttpAPI.py b/app/library/HttpAPI.py
index 287d4684..7c00ee2d 100644
--- a/app/library/HttpAPI.py
+++ b/app/library/HttpAPI.py
@@ -14,7 +14,6 @@ from app.library.Services import Services
from .cache import Cache
from .config import Config
-from .DownloadQueue import DownloadQueue
from .encoder import Encoder
from .Events import EventBus
from .router import RouteType, get_routes
@@ -24,8 +23,7 @@ LOG: logging.Logger = logging.getLogger("http_api")
class HttpAPI:
- def __init__(self, root_path: Path, queue: DownloadQueue):
- self.queue: DownloadQueue = queue or DownloadQueue.get_instance()
+ def __init__(self, root_path: Path):
self.encoder: Encoder = Encoder()
self.config: Config = Config.get_instance()
self._notify: EventBus = EventBus.get_instance()
@@ -33,12 +31,11 @@ class HttpAPI:
self.cache = Cache()
self.app: web.Application | None = None
- services = Services.get_instance()
+ services: Services = Services.get_instance()
services.add_all(
{
k: v
for k, v in {
- "queue": self.queue,
"encoder": self.encoder,
"config": self.config,
"notify": self._notify,
diff --git a/app/library/HttpSocket.py b/app/library/HttpSocket.py
index 16f3e4b2..b73ac236 100644
--- a/app/library/HttpSocket.py
+++ b/app/library/HttpSocket.py
@@ -11,7 +11,6 @@ from app.library.Services import Services
from app.library.Utils import load_modules
from .config import Config
-from .DownloadQueue import DownloadQueue
from .encoder import Encoder
from .Events import Event, EventBus, Events
from .ItemDTO import Item
@@ -26,19 +25,16 @@ class HttpSocket:
config: Config
sio: socketio.AsyncServer
- queue: DownloadQueue
di_context: dict[str, Any] = {}
def __init__(
self,
root_path: Path,
- queue: DownloadQueue | None = None,
encoder: Encoder | None = None,
config: Config | None = None,
sio: socketio.AsyncServer | None = None,
):
self.config = config or Config.get_instance()
- self.queue = queue or DownloadQueue.get_instance()
self._notify = EventBus.get_instance()
self.sio = sio or socketio.AsyncServer(
@@ -63,7 +59,6 @@ class HttpSocket:
k: v
for k, v in {
"config": self.config,
- "queue": self.queue,
"sio": self.sio,
"encoder": encoder,
"notify": self._notify,
@@ -104,7 +99,7 @@ class HttpSocket:
async def event_handler(data: Event, _):
if data and data.data:
- await self.queue.add(item=Item.format(data.data))
+ await Services.get_instance().get("queue").add(item=Item.format(data.data))
self._notify.subscribe(Events.ADD_URL, event_handler, f"{__class__.__name__}.add")
diff --git a/app/library/ItemDTO.py b/app/library/ItemDTO.py
index 62cc560b..cb75e43f 100644
--- a/app/library/ItemDTO.py
+++ b/app/library/ItemDTO.py
@@ -397,11 +397,8 @@ class ItemDTO:
dict: The serialized item.
"""
- if "finished" == self.status:
- if not self._recomputed:
- self.archive_status()
-
- self.get_file_sidecar()
+ if "finished" == self.status and not self._recomputed:
+ self.archive_status()
item, _ = clean_item(self.__dict__.copy(), ItemDTO.removed_fields())
return item
@@ -661,4 +658,3 @@ class ItemDTO:
self.get_archive_id()
self.get_archive_file()
self.archive_status()
- self.get_file_sidecar()
diff --git a/app/library/Tasks.py b/app/library/Tasks.py
index e6cf86da..349c38d8 100644
--- a/app/library/Tasks.py
+++ b/app/library/Tasks.py
@@ -828,11 +828,11 @@ class HandleTask:
if archive_file and archive_id in downloaded:
continue
- if download_queue.queue.exists(url=url):
+ if await download_queue.queue.exists(url=url):
continue
try:
- done = download_queue.done.get(url=url)
+ done = await download_queue.done.get(url=url)
if "error" != done.info.status:
continue
except KeyError:
diff --git a/app/library/config.py b/app/library/config.py
index f7d3aa05..bf1a82bf 100644
--- a/app/library/config.py
+++ b/app/library/config.py
@@ -311,20 +311,20 @@ class Config(metaclass=Singleton):
def __init__(self, is_native: bool = False):
baseDefaultPath: str = str(Path(__file__).parent.parent.parent.absolute())
- self.is_native = is_native
- self.temp_path = os.environ.get("YTP_TEMP_PATH", None) or str(Path(baseDefaultPath) / "var" / "tmp")
self.config_path = os.environ.get("YTP_CONFIG_PATH", None) or str(Path(baseDefaultPath) / "var" / "config")
- self.download_path = os.environ.get("YTP_DOWNLOAD_PATH", None) or str(
- Path(baseDefaultPath) / "var" / "downloads"
- )
- self.app_path = Path(__file__).parent.parent.absolute()
-
envFile: str = Path(self.config_path) / ".env"
if envFile.exists():
logging.info(f"Loading environment variables from '{envFile}'.")
load_dotenv(envFile)
+ self.is_native = is_native
+ self.temp_path = os.environ.get("YTP_TEMP_PATH", None) or str(Path(baseDefaultPath) / "var" / "tmp")
+ self.download_path = os.environ.get("YTP_DOWNLOAD_PATH", None) or str(
+ Path(baseDefaultPath) / "var" / "downloads"
+ )
+ self.app_path = Path(__file__).parent.parent.absolute()
+
for k, v in self._get_attributes().items():
if k.startswith("_") or k in self._manual_vars:
continue
@@ -441,6 +441,7 @@ class Config(metaclass=Singleton):
("urllib3.connectionpool", logging.WARNING),
("apprise", logging.WARNING),
("httpcore", logging.INFO),
+ ("aiosqlite", logging.INFO),
)
for _tool, _level in _log_levels:
logging.getLogger(_tool).setLevel(_level)
diff --git a/app/library/migrate.py b/app/library/migrate.py
new file mode 100644
index 00000000..1eca8b8f
--- /dev/null
+++ b/app/library/migrate.py
@@ -0,0 +1,321 @@
+# flake8: noqa: S608
+import contextlib
+import datetime
+import glob
+import os.path
+import traceback
+from collections.abc import AsyncIterator, Iterable, Sequence
+from importlib.machinery import ModuleSpec, SourceFileLoader
+from importlib.util import module_from_spec, spec_from_loader
+from typing import TYPE_CHECKING
+
+import aiosqlite
+
+if TYPE_CHECKING:
+ from types import ModuleType
+
+UTC_LENGTH = 14
+
+__version__ = "1.0.0"
+
+class Error(Exception):
+ """Base class for all Caribou errors."""
+
+
+class InvalidMigrationError(Error):
+ """Thrown when a client migration contains an error."""
+
+
+class InvalidNameError(Error):
+ """Thrown when a client migration has an invalid filename."""
+
+ def __init__(self, filename: str):
+ msg: str = (
+ f"Migration filenames must start with a UTC timestamp. The following file has an invalid name: {filename}"
+ )
+ super().__init__(msg)
+
+
+@contextlib.asynccontextmanager
+async def execute(
+ conn: aiosqlite.Connection, sql: str, params: Sequence[object] | None = None
+) -> AsyncIterator[aiosqlite.Cursor]:
+ params = [] if params is None else params
+ cursor = await conn.execute(sql, params)
+ try:
+ yield cursor
+ finally:
+ await cursor.close()
+
+
+@contextlib.asynccontextmanager
+async def transaction(conn: aiosqlite.Connection) -> AsyncIterator[None]:
+ try:
+ yield
+ await conn.commit()
+ except Exception:
+ await conn.rollback()
+ raise
+
+
+class Migration:
+ """This class represents a migration version."""
+
+ def __init__(self, path: str):
+ self.path: str = path
+ self.filename: str = os.path.basename(path)
+ self.module_name, _ = os.path.splitext(self.filename)
+ self.get_version() # will assert the filename is valid
+ self.name: str = self.module_name[UTC_LENGTH:]
+ while self.name.startswith("_"):
+ self.name: str = self.name[1:]
+
+ loader = SourceFileLoader(self.module_name, path)
+ spec: ModuleSpec | None = spec_from_loader(self.module_name, loader)
+ if spec is None:
+ msg = f"Cannot create spec for {path}"
+ raise ImportError(msg)
+
+ try:
+ module: ModuleType = module_from_spec(spec)
+ loader.exec_module(module)
+ self.module: ModuleType = module
+ except Exception as e:
+ msg: str = f"Invalid migration {path}: {traceback.format_exc()}"
+ raise InvalidMigrationError(msg) from e
+
+ targets: list[str] = ["upgrade", "downgrade"]
+ missing: list[str] = [m for m in targets if not self.has_method(m)]
+ if missing:
+ msg = f"Migration '{self.path}' is missing required methods: {', '.join(missing)}."
+ raise InvalidMigrationError(msg)
+
+ def get_version(self) -> str:
+ if len(self.filename) < UTC_LENGTH:
+ raise InvalidNameError(self.filename)
+ timestamp: str = self.filename[:UTC_LENGTH]
+ if not timestamp.isdigit():
+ raise InvalidNameError(self.filename)
+ return timestamp
+
+ async def upgrade(self, conn: aiosqlite.Connection) -> None:
+ await self.module.upgrade(conn)
+
+ async def downgrade(self, conn: aiosqlite.Connection) -> None:
+ await self.module.downgrade(conn)
+
+ def has_method(self, name: str) -> bool:
+ return callable(getattr(self.module, name, None))
+
+ def __repr__(self) -> str:
+ return f"Migration(path={self.filename})"
+
+
+class Database:
+ def __init__(self, db_url: aiosqlite.Connection | str, version_table: str = "migration_version"):
+ if not db_url:
+ msg = "Database requires db_url."
+ raise ValueError(msg)
+
+ self.version_table: str = version_table
+
+ self._owns_connection = bool(isinstance(db_url, str))
+ if self._owns_connection:
+ self.conn: aiosqlite.Connection | None = None
+ self.db_url: str = db_url
+ self._ensure_connection()
+ else:
+ self.conn: aiosqlite.Connection = db_url
+
+ async def __aenter__(self) -> "Database":
+ if self._owns_connection:
+ await self._ensure_connection()
+
+ return self
+
+ async def __aexit__(self, exc_type, exc, tb) -> None:
+ if not self._owns_connection:
+ return
+ await self.close()
+
+ async def close(self) -> None:
+ if self._owns_connection and self.conn:
+ await self.conn.close()
+ self.conn = None
+
+ async def _ensure_connection(self) -> None:
+ if self.conn:
+ return
+ self.conn = await aiosqlite.connect(self.db_url)
+ self.conn.row_factory = aiosqlite.Row
+
+ async def is_version_controlled(self) -> bool:
+ await self._ensure_connection()
+ assert self.conn
+ sql = """SELECT * FROM sqlite_master WHERE type = 'table' AND name = ?"""
+ async with execute(self.conn, sql, [self.version_table]) as cursor:
+ return bool(await cursor.fetchall())
+
+ async def upgrade(self, migrations: list[Migration], target_version: str | None = None) -> None:
+ await self._ensure_connection()
+ assert self.conn
+ if target_version:
+ _assert_migration_exists(migrations, target_version)
+
+ migrations.sort(key=lambda x: x.get_version())
+ database_version = await self.get_version()
+
+ for migration in migrations:
+ current_version: str = migration.get_version()
+ if database_version is not None and current_version <= database_version:
+ continue
+ if target_version and current_version > target_version:
+ break
+ await migration.upgrade(self.conn)
+ new_version: str = migration.get_version()
+ await self.update_version(new_version)
+ database_version: str = new_version
+
+ async def downgrade(self, migrations: list[Migration], target_version: str | int) -> None:
+ await self._ensure_connection()
+ assert self.conn
+ if target_version not in (0, "0"):
+ _assert_migration_exists(migrations, target_version)
+
+ migrations.sort(key=lambda x: x.get_version(), reverse=True)
+ database_version: str | None = await self.get_version()
+
+ for i, migration in enumerate(migrations):
+ current_version: str = migration.get_version()
+ if database_version is not None and current_version > database_version:
+ continue
+ if current_version <= target_version:
+ break
+ await migration.downgrade(self.conn)
+ next_version: str | int = 0
+ if i < len(migrations) - 1:
+ next_migration: Migration = migrations[i + 1]
+ next_version = next_migration.get_version()
+ await self.update_version(str(next_version))
+ database_version = str(next_version)
+
+ async def get_version(self) -> str | None:
+ """
+ Return the database's version, or None if it is not under version
+ control.
+ """
+ await self._ensure_connection()
+ assert self.conn
+ if not await self.is_version_controlled():
+ return None
+ sql: str = f"SELECT version FROM {self.version_table}"
+ async with execute(self.conn, sql) as cursor:
+ result = await cursor.fetchall()
+ return result[0][0] if result else "0"
+
+ async def update_version(self, version: str) -> None:
+ await self._ensure_connection()
+ assert self.conn
+ sql: str = f"UPDATE {self.version_table} SET version = ?"
+ async with transaction(self.conn):
+ await self.conn.execute(sql, [version])
+
+ async def initialize_version_control(self) -> None:
+ await self._ensure_connection()
+ assert self.conn
+ sql: str = f"""CREATE TABLE IF NOT EXISTS {self.version_table} ( version TEXT ) """
+ async with transaction(self.conn):
+ await self.conn.execute(sql)
+ await self.conn.execute(f"INSERT INTO {self.version_table} VALUES (0)")
+
+ def __repr__(self) -> str:
+ return f'Database("{self.db_url if self._owns_connection else "external_connection"}")'
+
+
+def _assert_migration_exists(migrations: Iterable[Migration], version: str | int) -> None:
+ if version not in (m.get_version() for m in migrations):
+ msg: str = f"No migration with version {version} exists."
+ raise Error(msg)
+
+
+def load_migrations(directory: str) -> list[Migration]:
+ """Return the migrations contained in the given directory."""
+ directory = str(directory)
+ if not os.path.exists(directory) or not os.path.isdir(directory):
+ msg: str = f"{directory} is not a directory."
+ raise Error(msg)
+ return [Migration(f) for f in glob.glob(os.path.join(directory, "*.py"))]
+
+
+async def upgrade(db_url: aiosqlite.Connection | str, migration_dir: str, version: str | None = None) -> None:
+ """
+ Upgrade the given database with the migrations contained in the
+ migrations directory. If a version is not specified, upgrade
+ to the most recent version.
+ """
+ async with Database(db_url) as db:
+ if not await db.is_version_controlled():
+ await db.initialize_version_control()
+
+ await db.upgrade(load_migrations(migration_dir), version)
+
+
+async def downgrade(db_url: str | aiosqlite.Connection, migration_dir: str, version: str) -> None:
+ """
+ Downgrade the database to the given version with the migrations
+ contained in the given migration directory.
+ """
+ async with Database(db_url) as db:
+ if not await db.is_version_controlled():
+ msg = f"The database {db_url} is not version controlled."
+ raise Error(msg)
+ migrations = load_migrations(migration_dir)
+ await db.downgrade(migrations, version)
+
+
+async def get_version(db_url: aiosqlite.Connection | str) -> str | None:
+ """Return the migration version of the given database."""
+ async with Database(db_url) as db:
+ return await db.get_version()
+
+
+def create_migration(name: str, directory: str | None = None) -> str:
+ """
+ Create a migration with the given name. If no directory is specified,
+ the current working directory will be used.
+ """
+ directory = directory if directory else "."
+ if not os.path.exists(directory) or not os.path.isdir(directory):
+ msg: str = f"{directory} is not a directory."
+ raise Error(msg)
+
+ now: datetime.datetime = datetime.datetime.now(tz=datetime.UTC)
+ version: str = now.strftime("%Y%m%d%H%M%S")
+
+ contents: str = MIGRATION_TEMPLATE % {"name": name, "version": version}
+
+ sanitized: str = name.replace(" ", "_")
+ filename: str = f"{version}_{sanitized}.py"
+ path: str = os.path.join(directory, filename)
+ with open(path, "w") as migration_file:
+ migration_file.write(contents)
+
+ return path
+
+
+MIGRATION_TEMPLATE = """\
+\"\"\"
+This module contains a db migration.
+
+Migration Name: %(name)s
+Migration Version: %(version)s
+\"\"\"
+
+async def upgrade(c):
+ # add your upgrade step here
+ await c.execute("SELECT 1")
+
+async def downgrade(c):
+ # add your downgrade step here
+ await c.execute("SELECT 1")
+"""
diff --git a/app/library/sqlite_store.py b/app/library/sqlite_store.py
new file mode 100644
index 00000000..7a1b7df3
--- /dev/null
+++ b/app/library/sqlite_store.py
@@ -0,0 +1,466 @@
+import asyncio
+import contextlib
+import json
+import logging
+import os
+from collections.abc import Iterable
+from dataclasses import fields
+from datetime import UTC, datetime
+from email.utils import formatdate
+
+import aiosqlite
+from aiohttp import web
+
+from .ItemDTO import ItemDTO
+from .operations import Operation, matches_condition
+from .Services import Services
+from .Singleton import ThreadSafe
+from .Utils import init_class
+
+LOG: logging.Logger = logging.getLogger(__name__)
+
+ITEM_DTO_FIELDS: set[str] = {f.name for f in fields(ItemDTO)}
+
+
+class Terminator:
+ pass
+
+
+class _Op:
+ """Queued write operation."""
+
+ __slots__ = ("item", "key", "keys", "op", "type_value")
+
+ def __init__(
+ self, op: Terminator | str, type_value: str, item: ItemDTO | None, key: str | None, keys: list[str] | None
+ ):
+ self.op = op
+ self.type_value = type_value
+ self.item = item
+ self.key = key
+ self.keys = keys
+
+
+class SqliteStore(metaclass=ThreadSafe):
+ @staticmethod
+ def get_instance(db_path: str | None = None) -> "SqliteStore":
+ return SqliteStore(db_path=db_path)
+
+ def attach(self, app: web.Application):
+ Services.get_instance().add("sqlite_store", self)
+ app.on_shutdown.append(self.on_shutdown)
+
+ async def on_shutdown(self, _: web.Application):
+ LOG.debug("Shutting down SqliteStore...")
+ await self.close()
+ LOG.debug("SqliteStore shut down complete.")
+
+ def __init__(self, db_path: str, *, max_pending: int = 200, flush_interval: float = 0.05):
+ self._db_path: str = db_path
+ self._conn: aiosqlite.Connection | None = None
+ self._queue: asyncio.Queue[_Op] | None = None
+ self._task: asyncio.Task | None = None
+ self._flush_interval: float = flush_interval
+ self._max_pending: int = max_pending
+ self._lock = asyncio.Lock()
+
+ async def __aenter__(self) -> "SqliteStore":
+ await self._ensure_conn()
+ return self
+
+ async def __aexit__(self, exc_type, exc, tb) -> None:
+ await self.close()
+
+ async def fetch_saved(self, type_value: str) -> list[tuple[str, ItemDTO]]:
+ await self._ensure_conn()
+ cursor = await self._conn.execute(
+ 'SELECT "id", "data", "created_at" FROM "history" WHERE "type" = ? ORDER BY "created_at" ASC',
+ (type_value,),
+ )
+ items: list[tuple[str, ItemDTO]] = []
+ async with cursor:
+ async for row in cursor:
+ row_date = datetime.strptime(row["created_at"], "%Y-%m-%d %H:%M:%S") # noqa: DTZ007
+ data = json.loads(row["data"])
+ data.pop("_id", None)
+ item = init_class(ItemDTO, data, ITEM_DTO_FIELDS)
+ item._id = row["id"]
+ item.datetime = formatdate(row_date.replace(tzinfo=UTC).timestamp())
+ items.append((row["id"], item))
+ return items
+
+ async def exists(self, type_value: str, key: str | None = None, url: str | None = None) -> bool:
+ return await self.get(type_value, key=key, url=url) is not None
+
+ async def get(self, type_value: str, key: str | None = None, url: str | None = None) -> bool:
+ if not key and not url:
+ msg = "key or url must be provided."
+ raise KeyError(msg)
+
+ await self._ensure_conn()
+
+ clauses: list[str] = []
+ params: list[str] = []
+
+ if key:
+ clauses.append('"id" = ?')
+ params.append(key)
+
+ if url:
+ clauses.append("json_extract(data, '$.url') = ?")
+ params.append(url)
+
+ where_clause = " OR ".join(clauses)
+ query = f'SELECT "id", "data", "created_at" FROM "history" WHERE "type" = ? AND ({where_clause}) LIMIT 1' # noqa: S608
+ cursor = await self._conn.execute(query, (type_value, *params))
+ row = await cursor.fetchone()
+ if not row:
+ return None
+
+ row_date = datetime.strptime(row["created_at"], "%Y-%m-%d %H:%M:%S") # noqa: DTZ007
+ data = json.loads(row["data"])
+ data.pop("_id", None)
+ item = init_class(ItemDTO, data, ITEM_DTO_FIELDS)
+ item._id = row["id"]
+ item.datetime = formatdate(row_date.replace(tzinfo=UTC).timestamp())
+ return item
+
+ async def get_by_id(self, type_value: str, id: str) -> ItemDTO | None:
+ await self._ensure_conn()
+ cursor = await self._conn.execute(
+ 'SELECT "id", "data", "created_at" FROM "history" WHERE "type" = ? AND "id" = ?',
+ (type_value, id),
+ )
+ row = await cursor.fetchone()
+ if not row:
+ return None
+
+ row_date = datetime.strptime(row["created_at"], "%Y-%m-%d %H:%M:%S") # noqa: DTZ007
+ data = json.loads(row["data"])
+ data.pop("_id", None)
+ item = init_class(ItemDTO, data, ITEM_DTO_FIELDS)
+ item._id = row["id"]
+ item.datetime = formatdate(row_date.replace(tzinfo=UTC).timestamp())
+ return item
+
+ async def get_item(self, type_value: str, **kwargs) -> ItemDTO | None:
+ """
+ Return first item of type matching *any* condition.
+
+ Mirrors :meth:`DataStore.get_item` semantics: if any provided condition
+ matches (OR logic) return the first row by creation time. Returns None
+ when kwargs is empty or no row matches.
+ """
+ if not kwargs:
+ return None
+
+ await self._ensure_conn()
+
+ clauses: list[str] = []
+ params: list[str | float | int] = []
+
+ def _safe_key(key: str) -> str | None:
+ return key if key.replace("_", "").isalnum() else None
+
+ for key, raw_value in kwargs.items():
+ safe_key = _safe_key(key)
+ if not safe_key:
+ continue
+
+ if isinstance(raw_value, tuple) and len(raw_value) == 2:
+ operation, value = raw_value
+ else:
+ operation, value = Operation.EQUAL, raw_value
+
+ if isinstance(operation, str):
+ try:
+ operation = Operation(operation)
+ except ValueError:
+ operation = Operation.EQUAL
+
+ path = f"$.{safe_key}"
+ json_extract = f"json_extract(data, '{path}')"
+
+ if Operation.EQUAL == operation:
+ clauses.append(f"{json_extract} = ?")
+ params.append(value)
+ elif Operation.NOT_EQUAL == operation:
+ clauses.append(f"{json_extract} != ?")
+ params.append(value)
+ elif Operation.CONTAIN == operation:
+ clauses.append(f"{json_extract} LIKE ? ESCAPE '\\'")
+ params.append(f"%{value}%")
+ elif Operation.NOT_CONTAIN == operation:
+ clauses.append(f"({json_extract} IS NULL OR {json_extract} NOT LIKE ? ESCAPE '\\')")
+ params.append(f"%{value}%")
+ elif Operation.STARTS_WITH == operation:
+ clauses.append(f"{json_extract} LIKE ? ESCAPE '\\'")
+ params.append(f"{value}%")
+ elif Operation.ENDS_WITH == operation:
+ clauses.append(f"{json_extract} LIKE ? ESCAPE '\\'")
+ params.append(f"%{value}")
+ elif Operation.GREATER_THAN == operation:
+ clauses.append(f"{json_extract} > ?")
+ params.append(value)
+ elif Operation.LESS_THAN == operation:
+ clauses.append(f"{json_extract} < ?")
+ params.append(value)
+ elif Operation.GREATER_EQUAL == operation:
+ clauses.append(f"{json_extract} >= ?")
+ params.append(value)
+ elif Operation.LESS_EQUAL == operation:
+ clauses.append(f"{json_extract} <= ?")
+ params.append(value)
+
+ if not clauses:
+ return None
+
+ where_clause = " OR ".join(f"({clause})" for clause in clauses)
+ query = f'SELECT "id", "data", "created_at" FROM "history" WHERE "type" = ? AND ({where_clause}) ORDER BY "created_at" ASC LIMIT 1' # noqa: S608
+ cursor = await self._conn.execute(query, (type_value, *params))
+ row = await cursor.fetchone()
+ if not row:
+ return None
+
+ row_date = datetime.strptime(row["created_at"], "%Y-%m-%d %H:%M:%S") # noqa: DTZ007
+ data = json.loads(row["data"])
+ data.pop("_id", None)
+ item = init_class(ItemDTO, data, ITEM_DTO_FIELDS)
+ item._id = row["id"]
+ item.datetime = formatdate(row_date.replace(tzinfo=UTC).timestamp())
+
+ return item if any(matches_condition(k, v, item.__dict__) for k, v in kwargs.items()) else None
+
+ async def count(self, type_value: str, status_filter: str | None = None) -> int:
+ await self._ensure_conn()
+ where_clauses = ['"type" = ?']
+ query_params: list[str] = [type_value]
+
+ if status_filter:
+ if status_filter.startswith("!"):
+ status_value = status_filter[1:]
+ where_clauses.append("json_extract(data, '$.status') != ?")
+ query_params.append(status_value)
+ else:
+ where_clauses.append("json_extract(data, '$.status') = ?")
+ query_params.append(status_filter)
+
+ where_clause = " AND ".join(where_clauses)
+ count_query = f'SELECT COUNT(*) as count FROM "history" WHERE {where_clause}' # noqa: S608
+ cursor = await self._conn.execute(count_query, tuple(query_params))
+ row = await cursor.fetchone()
+ return row["count"] if row else 0
+
+ async def paginate(
+ self,
+ type_value: str,
+ page: int,
+ per_page: int,
+ order: str,
+ status_filter: str | None = None,
+ ) -> tuple[list[tuple[str, ItemDTO]], int, int, int]:
+ await self._ensure_conn()
+ where_clauses = ['"type" = ?']
+ query_params: list[str | int] = [type_value]
+
+ if status_filter:
+ if status_filter.startswith("!"):
+ status_value = status_filter[1:]
+ where_clauses.append("json_extract(data, '$.status') != ?")
+ query_params.append(status_value)
+ else:
+ where_clauses.append("json_extract(data, '$.status') = ?")
+ query_params.append(status_filter)
+
+ where_clause = " AND ".join(where_clauses)
+ count_query = f'SELECT COUNT(*) as count FROM "history" WHERE {where_clause}' # noqa: S608
+ count_cursor = await self._conn.execute(count_query, tuple(query_params))
+ count_row = await count_cursor.fetchone()
+ total_items = count_row["count"] if count_row else 0
+ total_pages = (total_items + per_page - 1) // per_page if total_items > 0 else 1
+
+ if page > total_pages and total_items > 0:
+ page = total_pages
+
+ offset = (page - 1) * per_page
+ query_params.extend([per_page, offset])
+
+ cursor = await self._conn.execute(
+ f'SELECT "id", "data", "created_at" FROM "history" WHERE {where_clause} ORDER BY "created_at" {order} LIMIT ? OFFSET ?', # noqa: S608
+ tuple(query_params),
+ )
+
+ items: list[tuple[str, ItemDTO]] = []
+ async with cursor:
+ async for row in cursor:
+ row_date = datetime.strptime(row["created_at"], "%Y-%m-%d %H:%M:%S") # noqa: DTZ007
+ data = json.loads(row["data"])
+ data.pop("_id", None)
+ item = init_class(ItemDTO, data, ITEM_DTO_FIELDS)
+ item._id = row["id"]
+ item.datetime = formatdate(row_date.replace(tzinfo=UTC).timestamp())
+ items.append((row["id"], item))
+
+ return items, total_items, page, total_pages
+
+ async def upsert(self, type_value: str, item: ItemDTO) -> None:
+ await self._ensure_conn()
+ await self._upsert_now(type_value, item)
+
+ async def delete(self, type_value: str, key: str) -> None:
+ await self._ensure_conn()
+ await self._conn.execute('DELETE FROM "history" WHERE "type" = ? AND "id" = ?', (type_value, key))
+
+ async def bulk_delete(self, type_value: str, keys: Iterable[str]) -> int:
+ await self._ensure_conn()
+ keys_list = list(keys)
+ if not keys_list:
+ return 0
+ placeholders = ",".join("?" for _ in keys_list)
+ params = [type_value, *keys_list]
+ cur = await self._conn.execute(
+ f'DELETE FROM "history" WHERE "type" = ? AND "id" IN ({placeholders})', # noqa: S608
+ tuple(params),
+ )
+ return cur.rowcount if cur else 0
+
+ async def commit(self) -> None:
+ if self._conn:
+ await self._conn.commit()
+
+ async def enqueue_upsert(self, type_value: str, item: ItemDTO) -> None:
+ await self._enqueue(_Op("upsert", type_value, item, None, None))
+
+ async def enqueue_delete(self, type_value: str, key: str) -> None:
+ await self._enqueue(_Op("delete", type_value, None, key, None))
+
+ async def enqueue_bulk_delete(self, type_value: str, keys: Iterable[str]) -> None:
+ await self._enqueue(_Op("bulk_delete", type_value, None, None, list(keys)))
+
+ async def flush(self) -> None:
+ if self._queue:
+ await self._queue.join()
+ if self._conn:
+ await self._conn.commit()
+
+ async def shutdown(self) -> None:
+ """Flush pending writes, stop writer task, and commit."""
+ if self._queue:
+ try:
+ await self._queue.put(_Op(Terminator(), "", None, None, None))
+ LOG.debug("Waiting for SqliteStore queue to drain...")
+ await asyncio.wait_for(self._queue.join(), timeout=2)
+ except TimeoutError:
+ LOG.warning("SqliteStore queue did not drain within timeout; forcing writer shutdown.")
+
+ if self._task:
+ LOG.debug("Waiting for SqliteStore writer task to finish...")
+ if not self._task.done():
+ self._task.cancel()
+ with contextlib.suppress(asyncio.CancelledError):
+ await asyncio.wait_for(self._task, timeout=2)
+
+ if self._conn:
+ LOG.debug("Committing final changes to SqliteStore...")
+ await self._conn.commit()
+
+ self._task = None
+ self._queue = None
+
+ async def close(self) -> None:
+ await self.shutdown()
+ if self._conn:
+ LOG.debug("Closing SqliteStore connection...")
+ await self._conn.close()
+ self._conn = None
+
+ async def _enqueue(self, op: _Op) -> None:
+ self._ensure_worker()
+ await self._queue.put(op)
+
+ def _ensure_worker(self) -> None:
+ if self._queue is None:
+ self._queue = asyncio.Queue(maxsize=self._max_pending)
+ self._task = asyncio.create_task(self._writer(), name="sqlite-store-writer")
+
+ async def _writer(self) -> None:
+ while True:
+ op = await self._queue.get()
+ if isinstance(op.op, Terminator):
+ self._queue.task_done()
+ break
+ try:
+ async with self._lock:
+ await self._apply(op)
+ except Exception as ex:
+ LOG.exception(ex)
+ finally:
+ self._queue.task_done()
+ await asyncio.sleep(self._flush_interval)
+
+ async def _apply(self, op: _Op) -> None:
+ await self._ensure_conn()
+ if op.op == "upsert" and op.item:
+ await self._upsert_now(op.type_value, op.item)
+ elif op.op == "delete" and op.key:
+ await self._conn.execute('DELETE FROM "history" WHERE "type" = ? AND "id" = ?', (op.type_value, op.key))
+ elif op.op == "bulk_delete" and op.keys:
+ placeholders = ",".join("?" for _ in op.keys)
+ params = [op.type_value, *op.keys]
+ await self._conn.execute(
+ f'DELETE FROM "history" WHERE "type" = ? AND "id" IN ({placeholders})', # noqa: S608
+ tuple(params),
+ )
+ await self._conn.commit()
+
+ async def _upsert_now(self, type_value: str, item: ItemDTO) -> None:
+ await self._ensure_conn()
+ sql = """
+ INSERT INTO "history" ("id", "type", "url", "data")
+ VALUES (?, ?, ?, ?)
+ ON CONFLICT DO UPDATE SET "type" = ?, "url" = ?, "data" = ?, created_at = ?
+ """
+ encoded = item.json()
+ await self._conn.execute(
+ sql.strip(),
+ (
+ item._id,
+ type_value,
+ item.url,
+ encoded,
+ type_value,
+ item.url,
+ encoded,
+ datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S"),
+ ),
+ )
+
+ async def _ensure_conn(self) -> None:
+ if self._conn:
+ return
+
+ if not self._db_path:
+ msg = "No database path specified for SqliteStore."
+ raise RuntimeError(msg)
+
+ from app.library import migrate
+ from app.main import ROOT_PATH
+
+ if not self._db_path.startswith(":memory"):
+ os.makedirs(os.path.dirname(self._db_path) or ".", exist_ok=True)
+
+ self._conn = await aiosqlite.connect(database=self._db_path, isolation_level=None)
+ self._conn.row_factory = aiosqlite.Row
+ version = await migrate.get_version(self._conn)
+ if version:
+ LOG.debug(f"DB Version: '{version}'.")
+
+ await migrate.upgrade(self._conn, ROOT_PATH / "migrations")
+ if not version:
+ version = await migrate.get_version(self._conn)
+ LOG.debug(f"DB Version after initial migration: '{version}'.")
+
+ await self._conn.execute("PRAGMA journal_mode=wal")
+ await self._conn.execute("PRAGMA busy_timeout=5000")
+ await self._conn.execute("PRAGMA foreign_keys=ON")
+ await self._conn.commit()
diff --git a/app/main.py b/app/main.py
index 121e4484..822e9cbe 100644
--- a/app/main.py
+++ b/app/main.py
@@ -9,10 +9,8 @@ if APP_ROOT not in sys.path:
import asyncio
import logging
-import sqlite3
from pathlib import Path
-import caribou
import magic
from aiohttp import web
@@ -28,6 +26,7 @@ from app.library.Notifications import Notification
from app.library.Presets import Presets
from app.library.Scheduler import Scheduler
from app.library.Services import Services
+from app.library.sqlite_store import SqliteStore
from app.library.TaskDefinitions import TaskDefinitions
from app.library.Tasks import Tasks
@@ -43,25 +42,11 @@ class Main:
self._config.set_app_path(str(ROOT_PATH))
self._app = web.Application()
self._app.on_shutdown.append(self.on_shutdown)
- self._background_worker = BackgroundWorker()
Services.get_instance().add("app", self._app)
- Services.get_instance().add("background_worker", self._background_worker)
self._check_folders()
- LOG.debug(f"DB Version: '{caribou.get_version(self._config.db_file)}'.")
- caribou.upgrade(self._config.db_file, ROOT_PATH / "migrations")
-
- connection = sqlite3.connect(database=self._config.db_file, isolation_level=None)
- connection.row_factory = sqlite3.Row
- connection.execute("PRAGMA journal_mode=wal")
-
- async def _close_connection(_):
- LOG.debug("Closing database connection.")
- connection.close()
- LOG.debug("Database connection closed.")
-
try:
db_file = Path(self._config.db_file)
if "600" != oct(db_file.stat().st_mode)[-3:]:
@@ -69,15 +54,12 @@ class Main:
except Exception:
pass
- self._queue = DownloadQueue(connection=connection)
- self._http = HttpAPI(root_path=ROOT_PATH, queue=self._queue)
- self._socket = HttpSocket(root_path=ROOT_PATH, queue=self._queue)
-
- self._app.on_cleanup.append(_close_connection)
+ self._http = HttpAPI(root_path=ROOT_PATH)
+ self._socket = HttpSocket(root_path=ROOT_PATH)
def _check_folders(self):
"""Check if the required folders exist and create them if they do not."""
- folders = (self._config.download_path, self._config.temp_path, self._config.config_path)
+ folders: tuple[str, str, str] = (self._config.download_path, self._config.temp_path, self._config.config_path)
for folder in folders:
folder = Path(folder)
@@ -124,19 +106,20 @@ class Main:
if self._config.debug:
EventBus.get_instance().debug_enable()
+ SqliteStore.get_instance(db_path=self._config.db_file).attach(self._app)
+ BackgroundWorker.get_instance().attach(self._app)
Scheduler.get_instance().attach(self._app)
self._socket.attach(self._app)
self._http.attach(self._app)
- self._queue.attach(self._app)
- Tasks.get_instance().attach(self._app)
Presets.get_instance().attach(self._app)
+ Tasks.get_instance().attach(self._app)
Notification.get_instance().attach(self._app)
Conditions.get_instance().attach(self._app)
DLFields.get_instance().attach(self._app)
TaskDefinitions.get_instance().attach(self._app)
- self._background_worker.attach(self._app)
+ DownloadQueue.get_instance().attach(self._app)
EventBus.get_instance().emit(
Events.LOADED,
@@ -148,6 +131,7 @@ class Main:
def started(_):
LOG.info("=" * 40)
LOG.info(f"YTPTube {self._config.app_version} - started on http://{host}:{port}{self._config.base_path}")
+ LOG.info(f"Download path: {self._config.download_path}")
if self._config.is_native:
LOG.info("Running in native mode.")
LOG.info("=" * 40)
diff --git a/app/migrations/20231115152938_initial.py b/app/migrations/20231115152938_initial.py
index 119de811..043327db 100644
--- a/app/migrations/20231115152938_initial.py
+++ b/app/migrations/20231115152938_initial.py
@@ -1,12 +1,12 @@
"""
-This module contains a Caribou migration.
+This module contains a db migration.
Migration Name: initial
Migration Version: 20231115152938
"""
-def upgrade(connection):
+async def upgrade(c):
sql = """
CREATE TABLE "history" (
"id" TEXT PRIMARY KEY UNIQUE NOT NULL,
@@ -16,24 +16,24 @@ def upgrade(connection):
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
"""
- connection.execute(sql)
+ await c.execute(sql)
sql = """
CREATE INDEX "history_type" ON "history" ("type");
"""
- connection.execute(sql)
+ await c.execute(sql)
sql = """
CREATE UNIQUE INDEX "history_url" ON "history" ("url");
"""
- connection.execute(sql)
+ await c.execute(sql)
- connection.commit()
+ await c.commit()
-def downgrade(connection):
+async def downgrade(c):
sql = """
DROP TABLE IF EXISTS "history";
"""
- connection.execute(sql)
+ await c.execute(sql)
diff --git a/app/migrations/20251116173731_add_status_index.py b/app/migrations/20251116173731_add_status_index.py
index 51b9d7f6..ff1e1f08 100644
--- a/app/migrations/20251116173731_add_status_index.py
+++ b/app/migrations/20251116173731_add_status_index.py
@@ -1,11 +1,11 @@
"""
-This module contains a Caribou migration.
+This module contains a db migration.
Migration Name: add_status_index
Migration Version: 20251116173731
"""
-def upgrade(connection):
+async def upgrade(connection):
"""
Add index on json_extract(data, '$.status') for better query performance.
@@ -15,13 +15,13 @@ def upgrade(connection):
sql = """
CREATE INDEX IF NOT EXISTS "history_status" ON "history" (json_extract("data", '$.status'));
"""
- connection.execute(sql)
+ await connection.execute(sql)
-def downgrade(connection):
+async def downgrade(connection):
"""
Remove the status index.
"""
sql = """
DROP INDEX IF EXISTS "history_status";
"""
- connection.execute(sql)
+ await connection.execute(sql)
diff --git a/app/routes/api/browser.py b/app/routes/api/browser.py
index 4066f0dd..80cf1257 100644
--- a/app/routes/api/browser.py
+++ b/app/routes/api/browser.py
@@ -397,12 +397,12 @@ async def path_actions(request: Request, config: Config, queue: DownloadQueue, n
for old_sidecar, new_sidecar in sidecar_renamed:
record(old_sidecar, ok=True, action=action, extra={"new_path": new_sidecar})
- if item := queue.done.get_item(filename=str(path.relative_to(config.download_path))):
+ if item := await queue.done.get_item(filename=str(path.relative_to(config.download_path))):
item.info.filename = str(renamed.relative_to(config.download_path))
if sidecar_renamed:
item.info.get_file_sidecar()
- queue.done.put(item)
+ await queue.done.put(item)
notify.emit(Events.ITEM_UPDATED, data=item.info)
if "delete" == action:
@@ -503,12 +503,12 @@ async def path_actions(request: Request, config: Config, queue: DownloadQueue, n
for old_sidecar, new_sidecar in sidecar_moved:
record(old_sidecar, ok=True, action=action, extra={"new_path": new_sidecar})
- if item := queue.done.get_item(filename=str(path.relative_to(config.download_path))):
+ if item := await queue.done.get_item(filename=str(path.relative_to(config.download_path))):
item.info.filename = str(moved.relative_to(config.download_path))
if sidecar_moved:
item.info.get_file_sidecar()
- queue.done.put(item)
+ await queue.done.put(item)
notify.emit(Events.ITEM_UPDATED, data=item.info)
return web.json_response(data=operations_status, status=web.HTTPOk.status_code)
diff --git a/app/routes/api/history.py b/app/routes/api/history.py
index 4a4ef53a..1255f9fe 100644
--- a/app/routes/api/history.py
+++ b/app/routes/api/history.py
@@ -47,30 +47,21 @@ async def items_list(request: Request, queue: DownloadQueue, encoder: Encoder, c
"""
from app.library.DataStore import StoreType
- store_type = request.query.get("type", "all").lower()
- stores: list[str] = ["all", StoreType.QUEUE.value, StoreType.HISTORY.value]
- if store_type not in stores:
+ store_type: str = request.query.get("type", "queue").lower()
+ try:
+ store_type: StoreType = StoreType.from_value(store_type)
+ except ValueError:
return web.json_response(
- data={"error": f"type must be one of {', '.join(stores)}."},
+ data={"error": f"type must be one of {', '.join(StoreType.all())}."},
status=web.HTTPBadRequest.status_code,
)
- # Legacy behavior: return all items without pagination, will be removed in future.
- if "all" == store_type:
- data: dict = {"queue": [], "history": []}
- q = queue.get()
-
- data["queue"].extend([q.get("queue", {}).get(k) for k in q.get("queue", {})])
- data["history"].extend([q.get("done", {}).get(k) for k in q.get("done", {})])
-
- return web.json_response(data=data, status=web.HTTPOk.status_code, dumps=encoder.encode)
-
- ds = queue.queue if store_type == StoreType.QUEUE.value else queue.done
+ ds = queue.queue if store_type == StoreType.QUEUE else queue.done
try:
page = int(request.query.get("page", 1))
per_page = int(request.query.get("per_page", config.default_pagination))
- order = request.query.get("order", "DESC").upper()
+ order: str = request.query.get("order", "DESC").upper()
except ValueError:
return web.json_response(
data={"error": "page and per_page must be valid integers."},
@@ -92,25 +83,38 @@ async def items_list(request: Request, queue: DownloadQueue, encoder: Encoder, c
status=web.HTTPBadRequest.status_code,
)
- # Parse status filter
status_filter = request.query.get("status", None)
- items, total, current_page, total_pages = ds.get_items_paginated(
+ items, total, current_page, total_pages = await ds.get_items_paginated(
page=page, per_page=per_page, order=order, status_filter=status_filter
)
- data = {
- "pagination": {
- "page": current_page,
- "per_page": per_page,
- "total": total,
- "total_pages": total_pages,
- "has_next": current_page < total_pages,
- "has_prev": current_page > 1,
- },
- "items": [item for _, item in items],
- }
- return web.json_response(data=data, status=web.HTTPOk.status_code, dumps=encoder.encode)
+ if store_type == StoreType.HISTORY:
+ for _, download in items:
+ if not download.info:
+ continue
+
+ try:
+ download.info.sidecar = download.get_file_sidecar()
+ except Exception:
+ download.info.sidecar = {}
+
+ return web.json_response(
+ data={
+ "type": store_type.value,
+ "pagination": {
+ "page": current_page,
+ "per_page": per_page,
+ "total": total,
+ "total_pages": total_pages,
+ "has_next": current_page < total_pages,
+ "has_prev": current_page > 1,
+ },
+ "items": [download.info for _, download in items],
+ },
+ status=web.HTTPOk.status_code,
+ dumps=encoder.encode,
+ )
@route("DELETE", "api/history/", "items_delete")
@@ -137,24 +141,26 @@ async def items_delete(request: Request, queue: DownloadQueue, encoder: Encoder)
ids = data.get("ids")
remove_file: bool = bool(data.get("remove_file", True))
- _type = data.get("type", data.get("where"))
+ storeType = data.get("type", data.get("where"))
- if not _type:
+ if not storeType:
return web.json_response(data={"error": "Type is required."}, status=web.HTTPBadRequest.status_code)
- if _type not in [StoreType.QUEUE.value, StoreType.HISTORY.value]:
+ try:
+ storeType: StoreType = StoreType.from_value(storeType)
+ except ValueError:
return web.json_response(
- data={"error": f"type must be '{StoreType.QUEUE.value}' or '{StoreType.HISTORY.value}'."},
+ data={"error": f"type must be one of '{StoreType.all()}'."},
status=web.HTTPBadRequest.status_code,
)
- ds = queue.queue if _type == StoreType.QUEUE.value else queue.done
+ ds = queue.queue if storeType == StoreType.QUEUE else queue.done
if ids:
return web.json_response(
data={
"items": await (
- queue.cancel(ids) if _type == StoreType.QUEUE.value else queue.clear(ids, remove_file=remove_file)
+ queue.cancel(ids) if storeType == StoreType.QUEUE else queue.clear(ids, remove_file=remove_file)
),
"deleted": len(ids),
},
@@ -174,7 +180,7 @@ async def items_delete(request: Request, queue: DownloadQueue, encoder: Encoder)
per_page = 1000
while True:
- items, _, current_page, total_pages = ds.get_items_paginated(
+ items, _, current_page, total_pages = await ds.get_items_paginated(
page=page, per_page=per_page, order="DESC", status_filter=status_filter
)
@@ -192,7 +198,7 @@ async def items_delete(request: Request, queue: DownloadQueue, encoder: Encoder)
data={
"items": await (
queue.cancel(items_to_delete)
- if _type == StoreType.QUEUE.value
+ if storeType == StoreType.QUEUE
else queue.clear(items_to_delete, remove_file=remove_file)
),
"deleted": len(items_to_delete),
@@ -221,7 +227,7 @@ async def item_view(request: Request, queue: DownloadQueue, encoder: Encoder) ->
if not id:
return web.json_response(data={"error": "id is required."}, status=web.HTTPBadRequest.status_code)
- item: Download | None = queue.done.get_by_id(id) or queue.queue.get_by_id(id)
+ item: Download | None = await queue.done.get_by_id(id) or await queue.queue.get_by_id(id)
if not item:
return web.json_response(data={"error": "item not found."}, status=web.HTTPNotFound.status_code)
@@ -231,6 +237,7 @@ async def item_view(request: Request, queue: DownloadQueue, encoder: Encoder) ->
info: dict = {
**item.info.serialize(),
"ffprobe": {},
+ "sidecar": {},
}
if "finished" == item.info.status and (filename := item.info.get_file()):
@@ -241,6 +248,11 @@ async def item_view(request: Request, queue: DownloadQueue, encoder: Encoder) ->
except Exception:
pass
+ try:
+ info["sidecar"] = item.info.get_file_sidecar()
+ except Exception:
+ pass
+
return web.json_response(data=info, status=web.HTTPOk.status_code, dumps=encoder.encode)
@@ -263,7 +275,7 @@ async def item_update(request: Request, queue: DownloadQueue, encoder: Encoder,
if not id:
return web.json_response(data={"error": "id is required."}, status=web.HTTPBadRequest.status_code)
- item: Download | None = queue.done.get_by_id(id)
+ item: Download | None = await queue.done.get_by_id(id)
if not item:
return web.json_response(data={"error": "item not found."}, status=web.HTTPNotFound.status_code)
@@ -285,7 +297,7 @@ async def item_update(request: Request, queue: DownloadQueue, encoder: Encoder,
LOG.debug(f"Updated '{k}' to '{v}' for '{item.info.id}'")
if updated:
- queue.done.put(item)
+ await queue.done.put(item)
notify.emit(Events.ITEM_UPDATED, data=item.info)
return web.json_response(
@@ -394,7 +406,7 @@ async def item_archive_add(request: Request, queue: DownloadQueue, notify: Event
return web.json_response(data={"error": "id is required."}, status=web.HTTPBadRequest.status_code)
try:
- item: Download | None = queue.done.get_by_id(id)
+ item: Download | None = await queue.done.get_by_id(id)
if not item:
return web.json_response(data={"error": f"item '{id}' not found."}, status=web.HTTPNotFound.status_code)
except KeyError:
@@ -425,7 +437,7 @@ async def item_archive_add(request: Request, queue: DownloadQueue, notify: Event
)
item.info.archive_status(force=True)
- queue.done.put(item, no_notify=True)
+ await queue.done.put(item, no_notify=True)
notify.emit(Events.ITEM_UPDATED, data=item.info)
return web.json_response(
@@ -452,7 +464,7 @@ async def item_archive_delete(request: Request, queue: DownloadQueue, notify: Ev
return web.json_response(data={"error": "id is required."}, status=web.HTTPBadRequest.status_code)
try:
- item: Download | None = queue.done.get_by_id(id)
+ item: Download | None = await queue.done.get_by_id(id)
if not item:
return web.json_response(data={"error": f"item '{id}' not found."}, status=web.HTTPNotFound.status_code)
except KeyError:
@@ -483,7 +495,7 @@ async def item_archive_delete(request: Request, queue: DownloadQueue, notify: Ev
)
item.info.archive_status(force=True)
- queue.done.put(item, no_notify=True)
+ await queue.done.put(item, no_notify=True)
notify.emit(Events.ITEM_UPDATED, data=item.info)
return web.json_response(
diff --git a/app/routes/socket/connection.py b/app/routes/socket/connection.py
index 64586289..38e058ae 100644
--- a/app/routes/socket/connection.py
+++ b/app/routes/socket/connection.py
@@ -44,14 +44,22 @@ async def connect(config: Config, queue: DownloadQueue, notify: EventBus, sid: s
base=Path(config.download_path),
depth_limit=config.download_path_depth - 1,
),
- "history_count": queue.done.get_total_count(),
- "queue": queue.get("queue")["queue"],
+ "history_count": await queue.done.get_total_count(),
+ "queue": (await queue.get("queue"))["queue"],
},
title="Sending initial download data",
message=f"Sending initial download data to client '{sid}'.",
to=sid,
)
+ notify.emit(
+ Events.ACTIVE_QUEUE,
+ data={"queue": (await queue.get("queue"))["queue"]},
+ title="Sending initial active queue data",
+ message=f"Sending active queue data to client '{sid}'.",
+ to=sid,
+ )
+
@route(RouteType.SOCKET, "disconnect", "socket_disconnect")
async def disconnect(sio: socketio.AsyncServer, sid: str, data: str = None):
diff --git a/app/scripts/create_migration.py b/app/scripts/create_migration.py
new file mode 100644
index 00000000..c3ac4447
--- /dev/null
+++ b/app/scripts/create_migration.py
@@ -0,0 +1,147 @@
+#!/usr/bin/env python3
+"""
+Caribou migrations CLI.
+
+Provides commands to create, list, and apply database migrations.
+"""
+
+from __future__ import annotations
+
+import argparse
+import sys
+import traceback
+from pathlib import Path
+from typing import TYPE_CHECKING
+
+APP_ROOT: Path = Path(__file__).resolve().parents[2]
+if str(APP_ROOT) not in sys.path:
+ sys.path.insert(0, str(APP_ROOT))
+
+from app.library import migrate
+
+if TYPE_CHECKING:
+ from collections.abc import Callable
+
+
+def e_print(msg: str = "") -> None:
+ sys.stderr.write(f"{msg}\n")
+
+
+def o_print(msg: str = "") -> None:
+ sys.stdout.write(f"{msg}\n")
+
+
+def cmd_info(_: argparse.Namespace) -> None:
+ o_print(f"Version: {migrate.__version__}")
+
+
+def cmd_create(args: argparse.Namespace) -> None:
+ path = migrate.create_migration(args.name, args.migration_dir)
+ o_print(f"created migration {path}")
+
+
+def cmd_version(args: argparse.Namespace) -> None:
+ version = migrate.get_version(args.database_path)
+ if version:
+ o_print(f"the db [{args.database_path}] is at version {version}")
+ else:
+ o_print(f"the db [{args.database_path}] is not under version control")
+
+
+def cmd_upgrade(args: argparse.Namespace) -> None:
+ if args.version:
+ o_print(f"upgrading db [{args.database_path}] to version [{args.version}]")
+ else:
+ o_print(f"upgrading db [{args.database_path}] to most recent version")
+
+ migrate.upgrade(args.database_path, args.migration_dir, args.version)
+
+ new_version = migrate.get_version(args.database_path)
+ if args.version is not None:
+ assert new_version == args.version
+
+ o_print(f"upgraded [{args.database_path}] successfully to version [{new_version}]")
+
+
+def cmd_downgrade(args: argparse.Namespace) -> None:
+ o_print(f"downgrading db [{args.database_path}] to version [{args.version}]")
+ migrate.downgrade(args.database_path, args.migration_dir, args.version)
+ o_print(f"downgraded [{args.database_path}] successfully to version [{args.version}]")
+
+
+def cmd_list(args: argparse.Namespace) -> None:
+ o_print(f"Migrations in [{args.migration_dir}]:\n")
+ for m in migrate.load_migrations(args.migration_dir):
+ o_print(f"{m.get_version()}\t{m.name}\t{m.path}")
+
+
+def build_parser() -> argparse.ArgumentParser:
+ p = argparse.ArgumentParser(prog="caribou")
+ sub = p.add_subparsers(dest="command", required=True)
+
+ info = sub.add_parser("info", help="print library version")
+ info.set_defaults(func=cmd_info)
+
+ create = sub.add_parser("create", help="create a new migration file")
+ create.add_argument("name", help="the name of migration")
+ create.add_argument("-d", "--migration-dir", default=".", help="the migration directory")
+ create.set_defaults(func=cmd_create)
+
+ ver = sub.add_parser("version", help="return the migration version of the database")
+ ver.add_argument("database_path", help="path to the sqlite database")
+ ver.set_defaults(func=cmd_version)
+
+ up = sub.add_parser(
+ "upgrade",
+ help="upgrade the db (if version isn't specified, upgrade to the most recent)",
+ )
+ up.add_argument("database_path", help="path to the sqlite database")
+ up.add_argument("migration_dir", help="the migration directory")
+ up.add_argument("-v", "--version", type=int, default=None, help="the target migration version")
+ up.set_defaults(func=cmd_upgrade)
+
+ down = sub.add_parser(
+ "downgrade",
+ help="downgrade the db to a particular version (use 0 to rollback all changes)",
+ )
+ down.add_argument("database_path", help="path to the sqlite database")
+ down.add_argument("migration_dir", help="the migration directory")
+ down.add_argument("version", type=int, help="the target migration version")
+ down.set_defaults(func=cmd_downgrade)
+
+ lst = sub.add_parser("list", help="list the migration versions")
+ lst.add_argument("migration_dir", help="the migration directory")
+ lst.set_defaults(func=cmd_list)
+
+ return p
+
+
+def run(argv: list[str]) -> int:
+ parser = build_parser()
+ args = parser.parse_args(argv)
+
+ try:
+ func: Callable[[argparse.Namespace], None] = args.func
+ func(args)
+ return 0
+
+ except migrate.InvalidMigrationError:
+ e_print(traceback.format_exc())
+ return 1
+
+ except migrate.Error as err:
+ e_print(f"Error: {err}")
+ return 1
+
+ except Exception:
+ e_print("an unexpected error occurred:")
+ e_print(traceback.format_exc())
+ return 1
+
+
+def main() -> int:
+ return run(sys.argv[1:])
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/app/scripts/seed_db.py b/app/scripts/seed_db.py
index 6871118c..d3539104 100644
--- a/app/scripts/seed_db.py
+++ b/app/scripts/seed_db.py
@@ -28,6 +28,7 @@ LOG = logging.getLogger("seed_db")
USED_IDS: set[str] = set()
+
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Seed history/queue with synthetic records for performance testing.")
parser.add_argument(
@@ -141,13 +142,21 @@ def _build_row(
"live_in": None,
"file_size": stat.st_size,
"options": {},
- "extras": {},
+ "extras": {
+ "source_name": "Tester",
+ "source_id": "0ddc40f3-9227-4ffa-9f3c-c52bcf33ef2f",
+ "source_handler": "YoutubeHandler",
+ "metadata": {
+ "published": formatdate(timestamp.timestamp()),
+ },
+ "is_video": True,
+ "is_audio": True,
+ },
"cli": "",
"auto_start": True,
"is_archivable": True,
"is_archived": True,
"archive_id": archive_id,
- "sidecar": {},
"tmpfilename": None,
"filename": filename,
"total_bytes": stat.st_size,
@@ -157,8 +166,6 @@ def _build_row(
"percent": 100,
"speed": None,
"eta": None,
- "_recomputed": True,
- "_archive_file": None,
}
created_at = timestamp.strftime("%Y-%m-%d %H:%M:%S")
diff --git a/app/tests/test_datastore.py b/app/tests/test_datastore.py
index d7f8a717..7d25261c 100644
--- a/app/tests/test_datastore.py
+++ b/app/tests/test_datastore.py
@@ -1,5 +1,5 @@
+import asyncio
import json
-import sqlite3
from collections import OrderedDict
from dataclasses import asdict
from datetime import UTC, datetime
@@ -10,13 +10,50 @@ import pytest
from app.library.DataStore import DataStore, StoreType
from app.library.ItemDTO import ItemDTO
from app.library.operations import Operation
+from app.library.sqlite_store import SqliteStore
+
+
+async def make_db(data: int = 0) -> SqliteStore:
+ """Create a temporary database with test data."""
+ SqliteStore._reset_singleton()
+ ins = SqliteStore.get_instance(db_path=":memory:")
+ await ins._ensure_conn()
+
+ base_time = datetime.now(UTC)
+ for i in range(data):
+ created_at: str = base_time.replace(hour=(i // 4) % 24, minute=(i * 15) % 60, second=i % 60).strftime(
+ "%Y-%m-%d %H:%M:%S"
+ )
+
+ item_data = {
+ "url": f"https://example.com/video{i}",
+ "title": f"Test Video {i}",
+ "id": f"video{i}",
+ "folder": "/downloads",
+ "status": "finished",
+ }
+
+ await ins._conn.execute(
+ 'INSERT INTO "history" ("id", "type", "url", "data", "created_at") VALUES (?, ?, ?, ?, ?)',
+ (
+ f"test-id-{i}",
+ str(StoreType.HISTORY),
+ item_data["url"],
+ json.dumps(item_data),
+ created_at,
+ ),
+ )
+ if data > 0:
+ await ins._conn.commit()
+
+ return ins
class StubDownload:
def __init__(self, info: ItemDTO, started: bool = False, cancelled: bool = False):
self.info = info
- self._started = started
- self._cancelled = cancelled
+ self._started: bool = started
+ self._cancelled: bool = cancelled
def started(self) -> bool:
return self._started
@@ -25,11 +62,8 @@ class StubDownload:
return self._cancelled
-def make_conn() -> sqlite3.Connection:
- conn = sqlite3.connect(":memory:")
- conn.row_factory = sqlite3.Row
- conn.execute("CREATE TABLE history (id TEXT PRIMARY KEY, type TEXT, url TEXT, data TEXT, created_at TEXT)")
- return conn
+async def make_store_async(store_type: StoreType) -> DataStore:
+ return DataStore(store_type, await make_db())
def make_item(id: str, url: str = "http://u", title: str = "t", folder: str = "f") -> ItemDTO:
@@ -47,20 +81,23 @@ class TestStoreType:
class TestDataStore:
- def test_saved_items_parses_rows(self) -> None:
- conn = make_conn()
- # Prepare a stored item JSON (without _id, it will be set from the row)
+ @pytest.mark.asyncio
+ async def test_saved_items_parses_rows(self) -> None:
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
+ db = store._connection
+
dto = make_item(id="ignore", url="http://x", title="Title", folder="F")
data = asdict(dto)
data.pop("_id", None)
created = datetime(2024, 1, 2, 3, 4, 5, tzinfo=UTC)
- conn.execute(
+ await db._conn.execute(
"INSERT INTO history (id, type, url, data, created_at) VALUES (?, ?, ?, ?, ?)",
("abc", "queue", "http://x", json.dumps(data), created.strftime("%Y-%m-%d %H:%M:%S")),
)
- store = DataStore(StoreType.QUEUE, conn)
+ await db._conn.commit()
- items = store.saved_items()
+ items = await store.saved_items()
assert len(items) == 1
key, item = items[0]
assert key == "abc"
@@ -69,67 +106,79 @@ class TestDataStore:
assert isinstance(item.datetime, str)
assert item.datetime == formatdate(created.timestamp())
- def test_put_and_delete_persist(self) -> None:
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ @pytest.mark.asyncio
+ async def test_put_and_delete_persist(self) -> None:
+ store = await make_store_async(StoreType.QUEUE)
+ db = store._connection
item = make_item(id="vid1")
d = StubDownload(info=item)
- ret = store.put(d)
+ ret = await store.put(d)
+ await db.flush()
assert ret is store._dict[item._id]
# Verify row written; JSON should not contain datetime field
- row = conn.execute("SELECT * FROM history WHERE id=?", (item._id,)).fetchone()
+ cur = await db._conn.execute("SELECT * FROM history WHERE id=?", (item._id,))
+ row = await cur.fetchone()
assert row is not None
assert row["type"] == "queue"
assert row["url"] == item.url
assert '"datetime"' not in row["data"]
+ assert row["id"] == item._id
# Delete and ensure removal
- store.delete(item._id)
- row2 = conn.execute("SELECT * FROM history WHERE id=?", (item._id,)).fetchone()
+ await store.delete(item._id)
+ await asyncio.sleep(0)
+ await db.flush()
+ cur2 = await db._conn.execute("SELECT * FROM history WHERE id=?", (item._id,))
+ row2 = await cur2.fetchone()
assert row2 is None
+ await db.close()
- def test_exists_and_get(self) -> None:
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ @pytest.mark.asyncio
+ async def test_exists_and_get(self) -> None:
+ store = await make_store_async(StoreType.QUEUE)
item = make_item(id="a1", url="http://u1")
d = StubDownload(info=item)
- store.put(d)
+ await store.put(d)
+ await store._connection.flush()
- assert store.exists(key=item._id) is True
- assert store.exists(url=item.url) is True
+ assert await store.exists(key=item._id) is True
+ assert await store.exists(url=item.url) is True
with pytest.raises(KeyError):
- store.exists()
+ await store.exists()
- got = store.get(key=item._id)
+ got = await store.get(key=item._id)
assert got.info._id == item._id
- got2 = store.get(url=item.url)
+ got2 = await store.get(url=item.url)
assert got2.info.url == item.url
with pytest.raises(KeyError):
- store.get()
+ await store.get()
with pytest.raises(KeyError):
- store.get(key="missing")
+ await store.get(key="missing")
+ await store._connection.close()
- def test_next_and_empty(self) -> None:
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ @pytest.mark.asyncio
+ async def test_next_and_empty(self) -> None:
+ store = await make_store_async(StoreType.QUEUE)
assert store.empty() is True
d1 = StubDownload(info=make_item(id="x1"))
d2 = StubDownload(info=make_item(id="x2"))
- store.put(d1)
- store.put(d2)
+ await store.put(d1)
+ await store.put(d2)
+ await store._connection.flush()
assert store.empty() is False
first_key, first_val = store.next()
assert first_key == d1.info._id
assert first_val.info._id == d1.info._id
+ await store._connection.close()
- def test_has_downloads_and_get_next_download(self) -> None:
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ @pytest.mark.asyncio
+ async def test_has_downloads_and_get_next_download(self) -> None:
+ store = await make_store_async(StoreType.QUEUE)
# One non-auto-start, one started, one cancelled, and one eligible
i1 = make_item(id="n1")
@@ -138,36 +187,40 @@ class TestDataStore:
i3 = make_item(id="c1")
i4 = make_item(id="ok1")
- store.put(StubDownload(info=i1, started=False))
- store.put(StubDownload(info=i2, started=True))
- store.put(StubDownload(info=i3, started=False, cancelled=True))
- store.put(StubDownload(info=i4, started=False))
+ await store.put(StubDownload(info=i1, started=False))
+ await store.put(StubDownload(info=i2, started=True))
+ await store.put(StubDownload(info=i3, started=False, cancelled=True))
+ await store.put(StubDownload(info=i4, started=False))
+ await store._connection.flush()
assert store.has_downloads() is True
nxt = store.get_next_download()
assert isinstance(nxt, StubDownload)
assert nxt.info._id == i4._id
+ await store._connection.close()
@pytest.mark.asyncio
async def test_test_method_executes_query(self) -> None:
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ store = await make_store_async(StoreType.QUEUE)
# Should not raise
ok = await store.test()
assert ok is True
- def test_get_item_returns_none_when_no_kwargs(self) -> None:
+ @pytest.mark.asyncio
+ async def test_get_item_returns_none_when_no_kwargs(self) -> None:
"""Test that get_item returns None when no kwargs provided."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
- result = store.get_item()
+ result = await store.get_item()
assert result is None
+ await db.close()
- def test_get_item_finds_by_single_attribute(self) -> None:
+ @pytest.mark.asyncio
+ async def test_get_item_finds_by_single_attribute(self) -> None:
"""Test that get_item correctly finds item by a single attribute."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
# Create items with different attributes
item1 = make_item(id="vid1", url="http://example.com/1", title="Video 1", folder="folder1")
@@ -178,30 +231,33 @@ class TestDataStore:
d1 = StubDownload(info=item1)
d2 = StubDownload(info=item2)
- store.put(d1)
- store.put(d2)
+ await store.put(d1)
+ await store.put(d2)
+ await store._connection.flush()
# Test finding by title
- result = store.get_item(title="Video 1")
+ result = await store.get_item(title="Video 1")
assert result is not None
assert result.info._id == "id1"
assert result.info.title == "Video 1"
# Test finding by folder
- result = store.get_item(folder="folder2")
+ result = await store.get_item(folder="folder2")
assert result is not None
assert result.info._id == "id2"
assert result.info.folder == "folder2"
# Test finding by url
- result = store.get_item(url="http://example.com/1")
+ result = await store.get_item(url="http://example.com/1")
assert result is not None
assert result.info._id == "id1"
+ await db.close()
- def test_get_item_finds_by_multiple_attributes(self) -> None:
+ @pytest.mark.asyncio
+ async def test_get_item_finds_by_multiple_attributes(self) -> None:
"""Test that get_item finds item when ANY of the provided attributes match."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
item1 = make_item(id="vid1", url="http://example.com/1", title="Video 1", folder="folder1")
item1._id = "id1"
@@ -211,47 +267,54 @@ class TestDataStore:
d1 = StubDownload(info=item1)
d2 = StubDownload(info=item2)
- store.put(d1)
- store.put(d2)
+ await store.put(d1)
+ await store.put(d2)
+ await store._connection.flush()
# Test finding by multiple attributes where one matches
- result = store.get_item(title="Video 1", folder="wrong_folder")
+ result = await store.get_item(title="Video 1", folder="wrong_folder")
assert result is not None
assert result.info._id == "id1"
# Test finding where second attribute matches
- result = store.get_item(title="Wrong Title", folder="folder2")
+ result = await store.get_item(title="Wrong Title", folder="folder2")
assert result is not None
assert result.info._id == "id2"
+ await db.close()
- def test_get_item_returns_none_when_no_match(self) -> None:
+ @pytest.mark.asyncio
+ async def test_get_item_returns_none_when_no_match(self) -> None:
"""Test that get_item returns None when no attributes match."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
item = make_item(id="vid1", url="http://example.com/1", title="Video 1", folder="folder1")
item._id = "id1"
d = StubDownload(info=item)
- store.put(d)
+ await store.put(d)
+ await store._connection.flush()
# Test with non-matching attribute
- result = store.get_item(title="Nonexistent Video")
+ result = await store.get_item(title="Nonexistent Video")
assert result is None
# Test with non-existent attribute key
- result = store.get_item(nonexistent_field="value")
+ result = await store.get_item(nonexistent_field="value")
assert result is None
+ await db.close()
- def test_get_item_skips_items_with_no_info(self) -> None:
+ @pytest.mark.asyncio
+ async def test_get_item_skips_items_with_no_info(self) -> None:
"""Test that get_item skips items that have no info attribute."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
# Create a valid item
item = make_item(id="vid1", url="http://example.com/1", title="Video 1")
item._id = "id1"
d = StubDownload(info=item)
- store.put(d)
+ await store.put(d)
+ await store._connection.flush()
# Manually add an item with None info
class BrokenDownload:
@@ -261,14 +324,16 @@ class TestDataStore:
store._dict["broken"] = BrokenDownload()
# Should still find the valid item
- result = store.get_item(title="Video 1")
+ result = await store.get_item(title="Video 1")
assert result is not None
assert result.info._id == "id1"
+ await db.close()
- def test_get_item_returns_first_match(self) -> None:
+ @pytest.mark.asyncio
+ async def test_get_item_returns_first_match(self) -> None:
"""Test that get_item returns the first matching item when multiple match."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
# Create multiple items with same title
item1 = make_item(id="vid1", url="http://example.com/1", title="Same Title", folder="folder1")
@@ -279,27 +344,31 @@ class TestDataStore:
d1 = StubDownload(info=item1)
d2 = StubDownload(info=item2)
- store.put(d1)
- store.put(d2)
+ await store.put(d1)
+ await store.put(d2)
+ await store._connection.flush()
# Should return first match (note: OrderedDict maintains insertion order)
- result = store.get_item(title="Same Title")
+ result = await store.get_item(title="Same Title")
assert result is not None
assert result.info._id == "id1"
+ await db.close()
- def test_init(self) -> None:
+ @pytest.mark.asyncio
+ async def test_init(self) -> None:
"""Test DataStore initialization."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ store = await make_store_async(StoreType.QUEUE)
assert store._type == StoreType.QUEUE
- assert store._connection is conn
+ assert store._connection is not None
assert isinstance(store._dict, OrderedDict)
assert len(store._dict) == 0
- def test_load(self) -> None:
+ @pytest.mark.asyncio
+ async def test_load(self) -> None:
"""Test loading items from database into memory."""
- conn = make_conn()
+ db = await make_db()
+ conn = db._conn
# Insert items directly into database
item1_data = asdict(make_item(id="vid1", url="http://example.com/1", title="Video 1"))
@@ -308,7 +377,7 @@ class TestDataStore:
item2_data.pop("_id", None)
created = datetime(2024, 1, 1, 12, 0, 0, tzinfo=UTC)
- conn.execute(
+ await conn.execute(
"INSERT INTO history (id, type, url, data, created_at) VALUES (?, ?, ?, ?, ?)",
(
"id1",
@@ -318,7 +387,7 @@ class TestDataStore:
created.strftime("%Y-%m-%d %H:%M:%S"),
),
)
- conn.execute(
+ await conn.execute(
"INSERT INTO history (id, type, url, data, created_at) VALUES (?, ?, ?, ?, ?)",
(
"id2",
@@ -328,21 +397,24 @@ class TestDataStore:
created.strftime("%Y-%m-%d %H:%M:%S"),
),
)
+ await conn.commit()
- # Create store and load
- store = DataStore(StoreType.QUEUE, conn)
+ store = DataStore(StoreType.QUEUE, db)
assert len(store._dict) == 0
- store.load()
+ await store.load()
assert len(store._dict) == 2
assert "id1" in store._dict
assert "id2" in store._dict
assert store._dict["id1"].info.url == "http://example.com/1"
assert store._dict["id2"].info.url == "http://example.com/2"
+ await db.close()
- def test_load_with_different_store_types(self) -> None:
+ @pytest.mark.asyncio
+ async def test_load_with_different_store_types(self) -> None:
"""Test that load only loads items matching the store type."""
- conn = make_conn()
+ db = await make_db()
+ conn = db._conn
# Insert items with different types
item1_data = asdict(make_item(id="vid1", url="http://example.com/1"))
@@ -351,7 +423,7 @@ class TestDataStore:
item2_data.pop("_id", None)
created = datetime(2024, 1, 1, 12, 0, 0, tzinfo=UTC)
- conn.execute(
+ await conn.execute(
"INSERT INTO history (id, type, url, data, created_at) VALUES (?, ?, ?, ?, ?)",
(
"id1",
@@ -361,7 +433,7 @@ class TestDataStore:
created.strftime("%Y-%m-%d %H:%M:%S"),
),
)
- conn.execute(
+ await conn.execute(
"INSERT INTO history (id, type, url, data, created_at) VALUES (?, ?, ?, ?, ?)",
(
"id2",
@@ -371,23 +443,26 @@ class TestDataStore:
created.strftime("%Y-%m-%d %H:%M:%S"),
),
)
+ await conn.commit()
# Load QUEUE store - should only get queue items
- queue_store = DataStore(StoreType.QUEUE, conn)
- queue_store.load()
+ queue_store = DataStore(StoreType.QUEUE, db)
+ await queue_store.load()
assert len(queue_store._dict) == 1
assert "id1" in queue_store._dict
# Load HISTORY store - should only get history items
- history_store = DataStore(StoreType.HISTORY, conn)
- history_store.load()
+ history_store = DataStore(StoreType.HISTORY, db)
+ await history_store.load()
assert len(history_store._dict) == 1
assert "id2" in history_store._dict
+ await db.close()
- def test_get_by_id(self) -> None:
+ @pytest.mark.asyncio
+ async def test_get_by_id(self) -> None:
"""Test getting item by ID."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
item1 = make_item(id="vid1", url="http://example.com/1", title="Video 1")
item1._id = "id1"
@@ -397,27 +472,30 @@ class TestDataStore:
d1 = StubDownload(info=item1)
d2 = StubDownload(info=item2)
- store.put(d1)
- store.put(d2)
+ await store.put(d1)
+ await store.put(d2)
+ await store._connection.flush()
# Test getting existing items
- result = store.get_by_id("id1")
+ result = await store.get_by_id("id1")
assert result is not None
assert result.info._id == "id1"
assert result.info.title == "Video 1"
- result = store.get_by_id("id2")
+ result = await store.get_by_id("id2")
assert result is not None
assert result.info._id == "id2"
# Test getting non-existent item
- result = store.get_by_id("nonexistent")
+ result = await store.get_by_id("nonexistent")
assert result is None
+ await db.close()
- def test_items(self) -> None:
+ @pytest.mark.asyncio
+ async def test_items(self) -> None:
"""Test getting all items as list of tuples."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
# Empty store
result = store.items()
@@ -432,8 +510,9 @@ class TestDataStore:
d1 = StubDownload(info=item1)
d2 = StubDownload(info=item2)
- store.put(d1)
- store.put(d2)
+ await store.put(d1)
+ await store.put(d2)
+ await store._connection.flush()
# Test getting all items
result = list(store.items())
@@ -447,36 +526,44 @@ class TestDataStore:
# Verify order is maintained (OrderedDict)
assert result[0][0] == "id1"
assert result[1][0] == "id2"
+ await db.close()
- def test_get_total_count_with_empty_store(self) -> None:
+ @pytest.mark.asyncio
+ async def test_get_total_count_with_empty_store(self) -> None:
"""Test get_total_count with empty datastore."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ store = await make_store_async(StoreType.QUEUE)
- count = store.get_total_count()
+ count = await store.get_total_count()
assert count == 0
+ await store._connection.close()
- def test_get_total_count_with_items(self) -> None:
+ @pytest.mark.asyncio
+ async def test_get_total_count_with_items(self) -> None:
"""Test get_total_count with items in database."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ store = await make_store_async(StoreType.QUEUE)
+ db = store._connection
+ conn = db._conn
# Add items directly to database
created = datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S")
for i in range(5):
item_data = asdict(make_item(id=f"vid{i}"))
item_data.pop("_id", None)
- conn.execute(
+ await conn.execute(
"INSERT INTO history (id, type, url, data, created_at) VALUES (?, ?, ?, ?, ?)",
(f"id{i}", str(StoreType.QUEUE), f"http://example.com/{i}", json.dumps(item_data), created),
)
+ await conn.commit()
- count = store.get_total_count()
+ count = await store.get_total_count()
assert count == 5
+ await store._connection.close()
- def test_get_total_count_respects_store_type(self) -> None:
+ @pytest.mark.asyncio
+ async def test_get_total_count_respects_store_type(self) -> None:
"""Test that get_total_count only counts items of the correct type."""
- conn = make_conn()
+ db = await make_db()
+ conn = db._conn
created = datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S")
@@ -484,30 +571,33 @@ class TestDataStore:
for i in range(3):
item_data = asdict(make_item(id=f"vid{i}"))
item_data.pop("_id", None)
- conn.execute(
+ await conn.execute(
"INSERT INTO history (id, type, url, data, created_at) VALUES (?, ?, ?, ?, ?)",
- (f"q{i}", str(StoreType.QUEUE), f"http://example.com/{i}", json.dumps(item_data), created),
+ (f"q{i}", str(StoreType.QUEUE), f"http://example.com/queue/{i}", json.dumps(item_data), created),
)
# Add 2 HISTORY items
for i in range(2):
item_data = asdict(make_item(id=f"vid{i}"))
item_data.pop("_id", None)
- conn.execute(
+ await conn.execute(
"INSERT INTO history (id, type, url, data, created_at) VALUES (?, ?, ?, ?, ?)",
- (f"h{i}", str(StoreType.HISTORY), f"http://example.com/{i}", json.dumps(item_data), created),
+ (f"h{i}", str(StoreType.HISTORY), f"http://example.com/history/{i}", json.dumps(item_data), created),
)
+ await conn.commit()
- queue_store = DataStore(StoreType.QUEUE, conn)
- assert queue_store.get_total_count() == 3
+ queue_store = DataStore(StoreType.QUEUE, db)
+ assert await queue_store.get_total_count() == 3
- history_store = DataStore(StoreType.HISTORY, conn)
- assert history_store.get_total_count() == 2
+ history_store = DataStore(StoreType.HISTORY, db)
+ assert await history_store.get_total_count() == 2
+ await db.close()
- def test_put_with_error_status_emits_event(self) -> None:
+ @pytest.mark.asyncio
+ async def test_put_with_error_status_emits_event(self) -> None:
"""Test that put() emits an event when item has error status."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
item = make_item(id="vid1")
item.status = "error"
@@ -517,13 +607,16 @@ class TestDataStore:
# We can't easily test event emission without mocking EventBus
# Just verify it doesn't crash
- result = store.put(d)
+ result = await store.put(d)
+ await store._connection.flush()
assert result is not None
+ await db.close()
- def test_put_with_no_notify_skips_event(self) -> None:
+ @pytest.mark.asyncio
+ async def test_put_with_no_notify_skips_event(self) -> None:
"""Test that put() with no_notify=True doesn't emit events."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
item = make_item(id="vid1")
item.status = "error"
@@ -532,274 +625,309 @@ class TestDataStore:
d = StubDownload(info=item)
# Should not emit event when no_notify=True
- result = store.put(d, no_notify=True)
+ result = await store.put(d, no_notify=True)
+ await store._connection.flush()
assert result is not None
assert result.info._id == item._id
+ await db.close()
- def test_delete_nonexistent_item(self) -> None:
+ @pytest.mark.asyncio
+ async def test_delete_nonexistent_item(self) -> None:
"""Test that deleting non-existent item doesn't raise error."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
# Should not raise error
- store.delete("nonexistent_id")
+ await store.delete("nonexistent_id")
# Verify nothing was deleted from database
- row = conn.execute("SELECT * FROM history WHERE id=?", ("nonexistent_id",)).fetchone()
+ await store._connection.flush()
+ cursor = await db._conn.execute("SELECT * FROM history WHERE id=?", ("nonexistent_id",))
+ row = await cursor.fetchone()
assert row is None
+ await db.close()
- def test_has_downloads_with_empty_dict(self) -> None:
+ @pytest.mark.asyncio
+ async def test_has_downloads_with_empty_dict(self) -> None:
"""Test has_downloads returns False when dict is empty."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
assert store.has_downloads() is False
+ await db.close()
- def test_has_downloads_with_no_eligible_downloads(self) -> None:
+ @pytest.mark.asyncio
+ async def test_has_downloads_with_no_eligible_downloads(self) -> None:
"""Test has_downloads returns False when no downloads are eligible."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
# Add item that's already started
item1 = make_item(id="vid1")
- store.put(StubDownload(info=item1, started=True))
+ await store.put(StubDownload(info=item1, started=True))
# Add item with auto_start=False
item2 = make_item(id="vid2")
item2.auto_start = False
- store.put(StubDownload(info=item2, started=False))
+ await store.put(StubDownload(info=item2, started=False))
+ await store._connection.flush()
assert store.has_downloads() is False
+ await db.close()
- def test_get_next_download_returns_none_when_empty(self) -> None:
+ @pytest.mark.asyncio
+ async def test_get_next_download_returns_none_when_empty(self) -> None:
"""Test get_next_download returns None when no eligible downloads."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
result = store.get_next_download()
assert result is None
+ await db.close()
- def test_get_next_download_skips_cancelled(self) -> None:
+ @pytest.mark.asyncio
+ async def test_get_next_download_skips_cancelled(self) -> None:
"""Test get_next_download skips cancelled downloads."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
# Add cancelled download
item1 = make_item(id="vid1")
item1._id = "id1"
- store.put(StubDownload(info=item1, started=False, cancelled=True))
+ await store.put(StubDownload(info=item1, started=False, cancelled=True))
# Add eligible download
item2 = make_item(id="vid2")
item2._id = "id2"
- store.put(StubDownload(info=item2, started=False, cancelled=False))
+ await store.put(StubDownload(info=item2, started=False, cancelled=False))
+ await store._connection.flush()
result = store.get_next_download()
assert result is not None
assert result.info._id == "id2"
+ await db.close()
- def test_update_store_item_removes_datetime_field(self) -> None:
+ @pytest.mark.asyncio
+ async def test_update_store_item_removes_datetime_field(self) -> None:
"""Test that _update_store_item removes datetime field before storage."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
item = make_item(id="vid1")
item.datetime = "Thu, 01 Jan 2024 12:00:00 GMT" # Add datetime field
d = StubDownload(info=item)
- store.put(d)
+ await store.put(d)
+ await store._connection.flush()
# Verify datetime field is not in stored JSON
- row = conn.execute("SELECT data FROM history WHERE id=?", (item._id,)).fetchone()
+ cursor = await db._conn.execute("SELECT data FROM history WHERE id=?", (item._id,))
+ row = await cursor.fetchone()
assert row is not None
data = json.loads(row["data"])
assert "datetime" not in data
+ await db.close()
- def test_update_store_item_removes_live_in_when_finished(self) -> None:
+ @pytest.mark.asyncio
+ async def test_update_store_item_removes_live_in_when_finished(self) -> None:
"""Test that _update_store_item removes live_in field when status is finished."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ store = await make_store_async(StoreType.QUEUE)
+ conn = store._connection
item = make_item(id="vid1")
item.status = "finished"
item.live_in = "PT5M" # Add live_in field
d = StubDownload(info=item)
- store.put(d)
+ await store.put(d)
+ await store._connection.flush()
# Verify live_in field is not in stored JSON when status is finished
- row = conn.execute("SELECT data FROM history WHERE id=?", (item._id,)).fetchone()
+ cursor = await conn._conn.execute("SELECT data FROM history WHERE id=?", (item._id,))
+ row = await cursor.fetchone()
assert row is not None
data = json.loads(row["data"])
assert "live_in" not in data
+ await store._connection.close()
- def test_update_store_item_keeps_live_in_when_not_finished(self) -> None:
+ @pytest.mark.asyncio
+ async def test_update_store_item_keeps_live_in_when_not_finished(self) -> None:
"""Test that _update_store_item keeps live_in field when status is not finished."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ store = await make_store_async(StoreType.QUEUE)
+ conn = store._connection
item = make_item(id="vid1")
item.status = "downloading"
item.live_in = "PT5M" # Add live_in field
d = StubDownload(info=item)
- store.put(d)
+ await store.put(d)
+ await store._connection.flush()
# Verify live_in field IS in stored JSON when status is not finished
- row = conn.execute("SELECT data FROM history WHERE id=?", (item._id,)).fetchone()
+ cursor = await conn._conn.execute("SELECT data FROM history WHERE id=?", (item._id,))
+ row = await cursor.fetchone()
assert row is not None
data = json.loads(row["data"])
assert "live_in" in data
assert data["live_in"] == "PT5M"
+ await store._connection.close()
+@pytest.mark.asyncio
class TestDataStoreOperations:
"""Test get_item with different comparison operations."""
- def test_operation_equal(self) -> None:
+ async def test_operation_equal(self) -> None:
"""Test EQUAL operation (default behavior)."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
item1 = make_item(id="vid1", title="Exact Match", folder="folder1")
item1._id = "id1"
- store.put(StubDownload(info=item1))
+ await store.put(StubDownload(info=item1))
# Test with explicit EQUAL operation
- result = store.get_item(title=(Operation.EQUAL, "Exact Match"))
+ result = await store.get_item(title=(Operation.EQUAL, "Exact Match"))
assert result is not None
assert result.info._id == "id1"
# Test default behavior (no operation specified)
- result = store.get_item(title="Exact Match")
+ result = await store.get_item(title="Exact Match")
assert result is not None
assert result.info._id == "id1"
# Test no match
- result = store.get_item(title=(Operation.EQUAL, "No Match"))
+ result = await store.get_item(title=(Operation.EQUAL, "No Match"))
assert result is None
+ await db.close()
- def test_operation_not_equal(self) -> None:
+ async def test_operation_not_equal(self) -> None:
"""Test NOT_EQUAL operation."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
item1 = make_item(id="vid1", title="Video 1", folder="folder1")
item1._id = "id1"
item2 = make_item(id="vid2", title="Video 2", folder="folder2")
item2._id = "id2"
- store.put(StubDownload(info=item1))
- store.put(StubDownload(info=item2))
+ await store.put(StubDownload(info=item1))
+ await store.put(StubDownload(info=item2))
# Find item where title is not "Video 1"
- result = store.get_item(title=(Operation.NOT_EQUAL, "Video 1"))
+ result = await store.get_item(title=(Operation.NOT_EQUAL, "Video 1"))
assert result is not None
assert result.info._id == "id2"
+ await db.close()
- def test_operation_contain(self) -> None:
+ async def test_operation_contain(self) -> None:
"""Test CONTAIN operation (substring match)."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
item1 = make_item(id="vid1", title="Python Tutorial Video", folder="folder1")
item1._id = "id1"
item2 = make_item(id="vid2", title="JavaScript Course", folder="folder2")
item2._id = "id2"
- store.put(StubDownload(info=item1))
- store.put(StubDownload(info=item2))
+ await store.put(StubDownload(info=item1))
+ await store.put(StubDownload(info=item2))
# Find item with "Python" in title
- result = store.get_item(title=(Operation.CONTAIN, "Python"))
+ result = await store.get_item(title=(Operation.CONTAIN, "Python"))
assert result is not None
assert result.info._id == "id1"
# Find item with "Tutorial" in title
- result = store.get_item(title=(Operation.CONTAIN, "Tutorial"))
+ result = await store.get_item(title=(Operation.CONTAIN, "Tutorial"))
assert result is not None
assert result.info._id == "id1"
# No match
- result = store.get_item(title=(Operation.CONTAIN, "Rust"))
+ result = await store.get_item(title=(Operation.CONTAIN, "Rust"))
assert result is None
+ await db.close()
- def test_operation_not_contain(self) -> None:
+ async def test_operation_not_contain(self) -> None:
"""Test NOT_CONTAIN operation."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
item1 = make_item(id="vid1", title="Python Tutorial", folder="folder1")
item1._id = "id1"
item2 = make_item(id="vid2", title="JavaScript Course", folder="folder2")
item2._id = "id2"
- store.put(StubDownload(info=item1))
- store.put(StubDownload(info=item2))
+ await store.put(StubDownload(info=item1))
+ await store.put(StubDownload(info=item2))
# Find item that doesn't contain "Python"
- result = store.get_item(title=(Operation.NOT_CONTAIN, "Python"))
+ result = await store.get_item(title=(Operation.NOT_CONTAIN, "Python"))
assert result is not None
assert result.info._id == "id2"
+ await db.close()
- def test_operation_starts_with(self) -> None:
+ async def test_operation_starts_with(self) -> None:
"""Test STARTS_WITH operation."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
item1 = make_item(id="vid1", title="Tutorial: Python Basics", folder="folder1")
item1._id = "id1"
item2 = make_item(id="vid2", title="Course: JavaScript", folder="folder2")
item2._id = "id2"
- store.put(StubDownload(info=item1))
- store.put(StubDownload(info=item2))
+ await store.put(StubDownload(info=item1))
+ await store.put(StubDownload(info=item2))
# Find item starting with "Tutorial"
- result = store.get_item(title=(Operation.STARTS_WITH, "Tutorial"))
+ result = await store.get_item(title=(Operation.STARTS_WITH, "Tutorial"))
assert result is not None
assert result.info._id == "id1"
# Find item starting with "Course"
- result = store.get_item(title=(Operation.STARTS_WITH, "Course"))
+ result = await store.get_item(title=(Operation.STARTS_WITH, "Course"))
assert result is not None
assert result.info._id == "id2"
# No match
- result = store.get_item(title=(Operation.STARTS_WITH, "Video"))
+ result = await store.get_item(title=(Operation.STARTS_WITH, "Video"))
assert result is None
+ await db.close()
- def test_operation_ends_with(self) -> None:
+ async def test_operation_ends_with(self) -> None:
"""Test ENDS_WITH operation."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
item1 = make_item(id="vid1", title="Learn Python", folder="folder1")
item1._id = "id1"
item2 = make_item(id="vid2", title="Learn JavaScript", folder="folder2")
item2._id = "id2"
- store.put(StubDownload(info=item1))
- store.put(StubDownload(info=item2))
+ await store.put(StubDownload(info=item1))
+ await store.put(StubDownload(info=item2))
# Find item ending with "Python"
- result = store.get_item(title=(Operation.ENDS_WITH, "Python"))
+ result = await store.get_item(title=(Operation.ENDS_WITH, "Python"))
assert result is not None
assert result.info._id == "id1"
# Find item ending with "JavaScript"
- result = store.get_item(title=(Operation.ENDS_WITH, "JavaScript"))
+ result = await store.get_item(title=(Operation.ENDS_WITH, "JavaScript"))
assert result is not None
assert result.info._id == "id2"
# No match
- result = store.get_item(title=(Operation.ENDS_WITH, "Course"))
+ result = await store.get_item(title=(Operation.ENDS_WITH, "Course"))
assert result is None
+ await db.close()
- def test_operation_greater_than(self) -> None:
+ async def test_operation_greater_than(self) -> None:
"""Test GREATER_THAN operation with numeric values."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
item1 = make_item(id="vid1", title="Video 1")
item1._id = "id1"
@@ -808,23 +936,24 @@ class TestDataStoreOperations:
item2._id = "id2"
item2.filesize = 2000
- store.put(StubDownload(info=item1))
- store.put(StubDownload(info=item2))
+ await store.put(StubDownload(info=item1))
+ await store.put(StubDownload(info=item2))
# Find item with filesize > 1500
- result = store.get_item(filesize=(Operation.GREATER_THAN, 1500))
+ result = await store.get_item(filesize=(Operation.GREATER_THAN, 1500))
assert result is not None
assert result.info._id == "id2"
# Find item with filesize > 500 (should return first match)
- result = store.get_item(filesize=(Operation.GREATER_THAN, 500))
+ result = await store.get_item(filesize=(Operation.GREATER_THAN, 500))
assert result is not None
assert result.info._id == "id1"
+ await db.close()
- def test_operation_less_than(self) -> None:
+ async def test_operation_less_than(self) -> None:
"""Test LESS_THAN operation."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
item1 = make_item(id="vid1", title="Video 1")
item1._id = "id1"
@@ -833,66 +962,69 @@ class TestDataStoreOperations:
item2._id = "id2"
item2.filesize = 2000
- store.put(StubDownload(info=item1))
- store.put(StubDownload(info=item2))
+ await store.put(StubDownload(info=item1))
+ await store.put(StubDownload(info=item2))
# Find item with filesize < 1500
- result = store.get_item(filesize=(Operation.LESS_THAN, 1500))
+ result = await store.get_item(filesize=(Operation.LESS_THAN, 1500))
assert result is not None
assert result.info._id == "id1"
+ await db.close()
- def test_operation_greater_equal(self) -> None:
+ async def test_operation_greater_equal(self) -> None:
"""Test GREATER_EQUAL operation."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
item1 = make_item(id="vid1", title="Video 1")
item1._id = "id1"
item1.filesize = 1000
- store.put(StubDownload(info=item1))
+ await store.put(StubDownload(info=item1))
# Test >= with exact match
- result = store.get_item(filesize=(Operation.GREATER_EQUAL, 1000))
+ result = await store.get_item(filesize=(Operation.GREATER_EQUAL, 1000))
assert result is not None
assert result.info._id == "id1"
# Test >= with less than
- result = store.get_item(filesize=(Operation.GREATER_EQUAL, 500))
+ result = await store.get_item(filesize=(Operation.GREATER_EQUAL, 500))
assert result is not None
assert result.info._id == "id1"
# Test >= with greater than
- result = store.get_item(filesize=(Operation.GREATER_EQUAL, 1500))
+ result = await store.get_item(filesize=(Operation.GREATER_EQUAL, 1500))
assert result is None
+ await db.close()
- def test_operation_less_equal(self) -> None:
+ async def test_operation_less_equal(self) -> None:
"""Test LESS_EQUAL operation."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
item1 = make_item(id="vid1", title="Video 1")
item1._id = "id1"
item1.filesize = 1000
- store.put(StubDownload(info=item1))
+ await store.put(StubDownload(info=item1))
# Test <= with exact match
- result = store.get_item(filesize=(Operation.LESS_EQUAL, 1000))
+ result = await store.get_item(filesize=(Operation.LESS_EQUAL, 1000))
assert result is not None
# Test <= with greater than
- result = store.get_item(filesize=(Operation.LESS_EQUAL, 1500))
+ result = await store.get_item(filesize=(Operation.LESS_EQUAL, 1500))
assert result is not None
# Test <= with less than
- result = store.get_item(filesize=(Operation.LESS_EQUAL, 500))
+ result = await store.get_item(filesize=(Operation.LESS_EQUAL, 500))
assert result is None
+ await db.close()
- def test_mixed_operations(self) -> None:
+ async def test_mixed_operations(self) -> None:
"""Test using multiple operations in a single query."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
item1 = make_item(id="vid1", title="Python Tutorial", folder="tutorials")
item1._id = "id1"
@@ -901,89 +1033,94 @@ class TestDataStoreOperations:
item3 = make_item(id="vid3", title="JavaScript Basics", folder="tutorials")
item3._id = "id3"
- store.put(StubDownload(info=item1))
- store.put(StubDownload(info=item2))
- store.put(StubDownload(info=item3))
+ await store.put(StubDownload(info=item1))
+ await store.put(StubDownload(info=item2))
+ await store.put(StubDownload(info=item3))
# Mix of operation and default (any match returns true)
- result = store.get_item(title=(Operation.CONTAIN, "Python"), folder="tutorials")
+ result = await store.get_item(title=(Operation.CONTAIN, "Python"), folder="tutorials")
assert result is not None
assert result.info._id == "id1"
# Mix where first condition matches
- result = store.get_item(title=(Operation.CONTAIN, "JavaScript"), folder="nonexistent")
+ result = await store.get_item(title=(Operation.CONTAIN, "JavaScript"), folder="nonexistent")
assert result is not None
assert result.info._id == "id3"
+ await db.close()
- def test_operation_with_none_values(self) -> None:
+ async def test_operation_with_none_values(self) -> None:
"""Test operations handle None values gracefully."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
item1 = make_item(id="vid1", title="Video 1")
item1._id = "id1"
item1.description = None
- store.put(StubDownload(info=item1))
+ await store.put(StubDownload(info=item1))
# CONTAIN with None field should return False
- result = store.get_item(description=(Operation.CONTAIN, "test"))
+ result = await store.get_item(description=(Operation.CONTAIN, "test"))
assert result is None
# NOT_CONTAIN with None field should return True
- result = store.get_item(description=(Operation.NOT_CONTAIN, "test"))
+ result = await store.get_item(description=(Operation.NOT_CONTAIN, "test"))
assert result is not None
# GREATER_THAN with None should return False
- result = store.get_item(description=(Operation.GREATER_THAN, 100))
+ result = await store.get_item(description=(Operation.GREATER_THAN, 100))
assert result is None
+ await db.close()
- def test_operation_with_invalid_comparisons(self) -> None:
+ async def test_operation_with_invalid_comparisons(self) -> None:
"""Test that invalid comparisons are handled gracefully."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
item1 = make_item(id="vid1", title="Video 1")
item1._id = "id1"
- store.put(StubDownload(info=item1))
+ await store.put(StubDownload(info=item1))
# Try to compare string with number using > (should return False/None)
- result = store.get_item(title=(Operation.GREATER_THAN, 100))
+ result = await store.get_item(title=(Operation.GREATER_THAN, 100))
assert result is None
+ await db.close()
- def test_operation_backward_compatibility(self) -> None:
+ async def test_operation_backward_compatibility(self) -> None:
"""Test that string operation names work for backward compatibility."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
item1 = make_item(id="vid1", title="Python Tutorial")
item1._id = "id1"
- store.put(StubDownload(info=item1))
+ await store.put(StubDownload(info=item1))
# Using string operation value
- result = store.get_item(title=("in", "Python"))
+ result = await store.get_item(title=("in", "Python"))
assert result is not None
assert result.info._id == "id1"
# Using string for EQUAL
- result = store.get_item(title=("==", "Python Tutorial"))
+ result = await store.get_item(title=("==", "Python Tutorial"))
assert result is not None
+ await db.close()
- def test_operation_with_nonexistent_field(self) -> None:
+ async def test_operation_with_nonexistent_field(self) -> None:
"""Test operations with fields that don't exist."""
- conn = make_conn()
- store = DataStore(StoreType.QUEUE, conn)
+ db = await make_db()
+ store = DataStore(StoreType.QUEUE, db)
item1 = make_item(id="vid1", title="Video 1")
item1._id = "id1"
- store.put(StubDownload(info=item1))
+ await store.put(StubDownload(info=item1))
# Try to match on non-existent field
- result = store.get_item(nonexistent_field=(Operation.EQUAL, "value"))
+ result = await store.get_item(nonexistent_field=(Operation.EQUAL, "value"))
assert result is None
- result = store.get_item(nonexistent_field=(Operation.CONTAIN, "value"))
+ result = await store.get_item(nonexistent_field=(Operation.CONTAIN, "value"))
assert result is None
+ await db.close()
diff --git a/app/tests/test_datastore_pagination.py b/app/tests/test_datastore_pagination.py
index da38f3f1..0c8c280b 100644
--- a/app/tests/test_datastore_pagination.py
+++ b/app/tests/test_datastore_pagination.py
@@ -1,257 +1,212 @@
import json
-import sqlite3
from datetime import UTC, datetime
-from pathlib import Path
-from tempfile import TemporaryDirectory
import pytest
+import pytest_asyncio
from app.library.DataStore import DataStore, StoreType
+from app.library.Download import Download
from app.library.ItemDTO import ItemDTO
+from app.library.sqlite_store import SqliteStore
+async def make_db(data: int = 100) -> SqliteStore:
+ """Create a temporary database with test data."""
+ SqliteStore._reset_singleton()
+ ins = SqliteStore.get_instance(db_path=":memory:")
+ await ins._ensure_conn()
+
+ base_time = datetime.now(UTC)
+ for i in range(data):
+ created_at = base_time.replace(hour=(i // 4) % 24, minute=(i * 15) % 60, second=i % 60).strftime(
+ "%Y-%m-%d %H:%M:%S"
+ )
+
+ item_data = {
+ "url": f"https://example.com/video{i}",
+ "title": f"Test Video {i}",
+ "id": f"video{i}",
+ "folder": "/downloads",
+ "status": "finished",
+ }
+
+ await ins._conn.execute(
+ 'INSERT INTO "history" ("id", "type", "url", "data", "created_at") VALUES (?, ?, ?, ?, ?)',
+ (
+ f"test-id-{i}",
+ str(StoreType.HISTORY),
+ item_data["url"],
+ json.dumps(item_data),
+ created_at,
+ ),
+ )
+ if data > 0:
+ await ins._conn.commit()
+
+ return ins
+
+
+@pytest.mark.asyncio
class TestDataStorePagination:
"""Test pagination functionality of DataStore."""
- @pytest.fixture
- def temp_db(self):
- """Create a temporary database with test data."""
- with TemporaryDirectory() as tmpdir:
- db_path = Path(tmpdir) / "test.db"
- conn = sqlite3.connect(str(db_path))
- conn.row_factory = sqlite3.Row
+ @pytest_asyncio.fixture
+ async def make_db(self, data: int = 100):
+ """Fixture to provide a temporary database with test data."""
+ return await make_db(data=data)
- # Create the history table
- conn.execute(
- """
- CREATE TABLE IF NOT EXISTS "history" (
- "id" TEXT PRIMARY KEY,
- "type" TEXT NOT NULL,
- "url" TEXT NOT NULL,
- "data" TEXT NOT NULL,
- "created_at" TEXT NOT NULL
- )
- """
- )
-
- # Insert test data
- base_time = datetime.now(UTC)
- for i in range(100):
- created_at = base_time.replace(
- hour=(i // 4) % 24, minute=(i * 15) % 60, second=i % 60
- ).strftime("%Y-%m-%d %H:%M:%S")
-
- item_data = {
- "url": f"https://example.com/video{i}",
- "title": f"Test Video {i}",
- "id": f"video{i}",
- "folder": "/downloads",
- "status": "finished",
- }
-
- conn.execute(
- 'INSERT INTO "history" ("id", "type", "url", "data", "created_at") VALUES (?, ?, ?, ?, ?)',
- (
- f"test-id-{i}",
- str(StoreType.HISTORY),
- item_data["url"],
- json.dumps(item_data),
- created_at,
- ),
- )
-
- conn.commit()
- yield conn
- conn.close()
-
- def test_get_total_count(self, temp_db):
+ async def test_get_total_count(self):
"""Test getting total count of items."""
- datastore = DataStore(type=StoreType.HISTORY, connection=temp_db)
- count = datastore.get_total_count()
- assert count == 100
+ db = await make_db(data=100)
+ try:
+ datastore = DataStore(type=StoreType.HISTORY, connection=db)
+ count = await datastore.get_total_count()
+ assert count == 100
+ finally:
+ await db.close()
- def test_pagination_basic(self, temp_db):
+ async def test_pagination_basic(self):
"""Test basic pagination functionality."""
- datastore = DataStore(type=StoreType.HISTORY, connection=temp_db)
+ db = await make_db(data=100)
+ try:
+ datastore = DataStore(type=StoreType.HISTORY, connection=db)
+ items, total, page, total_pages = await datastore.get_items_paginated(page=1, per_page=10)
+ assert len(items) == 10
+ assert total == 100
+ assert page == 1
+ assert total_pages == 10
+ for _, item in items:
+ assert isinstance(item, Download)
+ assert isinstance(item.info, ItemDTO)
+ assert item.info._id.startswith("test-id-")
+ finally:
+ await db.close()
- # Get first page
- items, total, page, total_pages = datastore.get_items_paginated(page=1, per_page=10)
-
- assert len(items) == 10
- assert total == 100
- assert page == 1
- assert total_pages == 10
-
- # Verify items are ItemDTO instances
- for _item_id, item in items:
- assert isinstance(item, ItemDTO)
- assert item._id.startswith("test-id-")
-
- def test_pagination_last_page(self, temp_db):
+ async def test_pagination_last_page(self):
"""Test pagination on last page."""
- datastore = DataStore(type=StoreType.HISTORY, connection=temp_db)
+ db = await make_db(data=100)
+ try:
+ datastore = DataStore(type=StoreType.HISTORY, connection=db)
+ items, total, page, total_pages = await datastore.get_items_paginated(page=10, per_page=10)
+ assert len(items) == 10
+ assert total == 100
+ assert page == 10
+ assert total_pages == 10
+ finally:
+ await db.close()
- # Get last page
- items, total, page, total_pages = datastore.get_items_paginated(page=10, per_page=10)
-
- assert len(items) == 10
- assert total == 100
- assert page == 10
- assert total_pages == 10
-
- def test_pagination_partial_page(self, temp_db):
+ async def test_pagination_partial_page(self):
"""Test pagination with partial last page."""
- datastore = DataStore(type=StoreType.HISTORY, connection=temp_db)
+ db = await make_db(data=100)
+ try:
+ datastore = DataStore(type=StoreType.HISTORY, connection=db)
+ items, total, page, total_pages = await datastore.get_items_paginated(page=4, per_page=30)
+ assert len(items) == 10 # 100 items / 30 per page = 3 full pages + 10 items
+ assert total == 100
+ assert page == 4
+ assert total_pages == 4
+ finally:
+ await db.close()
- # Get items with per_page that doesn't divide evenly
- items, total, page, total_pages = datastore.get_items_paginated(page=4, per_page=30)
-
- assert len(items) == 10 # 100 items / 30 per page = 3 full pages + 10 items
- assert total == 100
- assert page == 4
- assert total_pages == 4
-
- def test_pagination_out_of_range(self, temp_db):
+ async def test_pagination_out_of_range(self):
"""Test pagination with page number out of range."""
- datastore = DataStore(type=StoreType.HISTORY, connection=temp_db)
+ db = await make_db(data=100)
+ try:
+ datastore = DataStore(type=StoreType.HISTORY, connection=db)
+ items, total, page, total_pages = await datastore.get_items_paginated(page=999, per_page=10)
+ assert len(items) == 10
+ assert total == 100
+ assert page == 10 # Adjusted to last page
+ assert total_pages == 10
+ finally:
+ await db.close()
- # Request page beyond total pages - should return last page
- items, total, page, total_pages = datastore.get_items_paginated(page=999, per_page=10)
-
- assert len(items) == 10
- assert total == 100
- assert page == 10 # Adjusted to last page
- assert total_pages == 10
-
- def test_pagination_order_desc(self, temp_db):
+ async def test_pagination_order_desc(self):
"""Test pagination with descending order (newest first)."""
- datastore = DataStore(type=StoreType.HISTORY, connection=temp_db)
+ db = await make_db(data=100)
+ try:
+ datastore = DataStore(type=StoreType.HISTORY, connection=db)
+ items, _, _, _ = await datastore.get_items_paginated(page=1, per_page=5, order="DESC")
+ assert len(items) == 5
+ finally:
+ await db.close()
- items, _, _, _ = datastore.get_items_paginated(page=1, per_page=5, order="DESC")
-
- assert len(items) == 5
- # Verify order - should be newest to oldest
- # (note: exact order depends on the timestamp generation in fixture)
-
- def test_pagination_order_asc(self, temp_db):
+ async def test_pagination_order_asc(self):
"""Test pagination with ascending order (oldest first)."""
- datastore = DataStore(type=StoreType.HISTORY, connection=temp_db)
+ db = await make_db(data=100)
+ try:
+ datastore = DataStore(type=StoreType.HISTORY, connection=db)
+ items, _, _, _ = await datastore.get_items_paginated(page=1, per_page=5, order="ASC")
+ assert len(items) == 5
+ finally:
+ await db.close()
- items, _, _, _ = datastore.get_items_paginated(page=1, per_page=5, order="ASC")
-
- assert len(items) == 5
-
- def test_pagination_invalid_page(self, temp_db):
+ async def test_pagination_invalid_page(self):
"""Test pagination with invalid page number."""
- datastore = DataStore(type=StoreType.HISTORY, connection=temp_db)
+ db = await make_db(data=100)
+ try:
+ datastore = DataStore(type=StoreType.HISTORY, connection=db)
+ with pytest.raises(ValueError, match="page must be >= 1"):
+ await datastore.get_items_paginated(page=0, per_page=10)
- with pytest.raises(ValueError, match="page must be >= 1"):
- datastore.get_items_paginated(page=0, per_page=10)
+ with pytest.raises(ValueError, match="page must be >= 1"):
+ await datastore.get_items_paginated(page=-1, per_page=10)
+ finally:
+ await db.close()
- with pytest.raises(ValueError, match="page must be >= 1"):
- datastore.get_items_paginated(page=-1, per_page=10)
-
- def test_pagination_invalid_per_page(self, temp_db):
+ async def test_pagination_invalid_per_page(self):
"""Test pagination with invalid per_page value."""
- datastore = DataStore(type=StoreType.HISTORY, connection=temp_db)
+ db = await make_db(data=100)
+ try:
+ datastore = DataStore(type=StoreType.HISTORY, connection=db)
- with pytest.raises(ValueError, match="per_page must be >= 1"):
- datastore.get_items_paginated(page=1, per_page=0)
+ with pytest.raises(ValueError, match="per_page must be >= 1"):
+ await datastore.get_items_paginated(page=1, per_page=0)
- with pytest.raises(ValueError, match="per_page must be >= 1"):
- datastore.get_items_paginated(page=1, per_page=-10)
+ with pytest.raises(ValueError, match="per_page must be >= 1"):
+ await datastore.get_items_paginated(page=1, per_page=-10)
+ finally:
+ await db.close()
- def test_pagination_invalid_order(self, temp_db):
+ async def test_pagination_invalid_order(self):
"""Test pagination with invalid order parameter."""
- datastore = DataStore(type=StoreType.HISTORY, connection=temp_db)
+ db = await make_db(data=100)
+ try:
+ datastore = DataStore(type=StoreType.HISTORY, connection=db)
+ with pytest.raises(ValueError, match="order must be 'ASC' or 'DESC'"):
+ await datastore.get_items_paginated(page=1, per_page=10, order="INVALID")
+ finally:
+ await db.close()
- with pytest.raises(ValueError, match="order must be 'ASC' or 'DESC'"):
- datastore.get_items_paginated(page=1, per_page=10, order="INVALID")
-
- def test_pagination_empty_store(self):
+ async def test_pagination_empty_store(self):
"""Test pagination with empty datastore."""
- with TemporaryDirectory() as tmpdir:
- db_path = Path(tmpdir) / "empty.db"
- conn = sqlite3.connect(str(db_path))
- conn.row_factory = sqlite3.Row
-
- # Create empty table
- conn.execute(
- """
- CREATE TABLE IF NOT EXISTS "history" (
- "id" TEXT PRIMARY KEY,
- "type" TEXT NOT NULL,
- "url" TEXT NOT NULL,
- "data" TEXT NOT NULL,
- "created_at" TEXT NOT NULL
- )
- """
- )
- conn.commit()
-
- datastore = DataStore(type=StoreType.HISTORY, connection=conn)
-
- items, total, page, total_pages = datastore.get_items_paginated(page=1, per_page=10)
+ db = await make_db(data=0)
+ try:
+ datastore = DataStore(type=StoreType.HISTORY, connection=db)
+ items, total, page, total_pages = await datastore.get_items_paginated(page=1, per_page=10)
assert len(items) == 0
assert total == 0
assert page == 1
assert total_pages == 1
+ finally:
+ await db.close()
- conn.close()
-
- def test_pagination_single_item(self):
+ async def test_pagination_single_item(self):
"""Test pagination with single item."""
- with TemporaryDirectory() as tmpdir:
- db_path = Path(tmpdir) / "single.db"
- conn = sqlite3.connect(str(db_path))
- conn.row_factory = sqlite3.Row
-
- # Create table with single item
- conn.execute(
- """
- CREATE TABLE IF NOT EXISTS "history" (
- "id" TEXT PRIMARY KEY,
- "type" TEXT NOT NULL,
- "url" TEXT NOT NULL,
- "data" TEXT NOT NULL,
- "created_at" TEXT NOT NULL
- )
- """
- )
-
- item_data = {
- "url": "https://example.com/single",
- "title": "Single Video",
- "id": "single-video",
- "folder": "/downloads",
- "status": "finished",
- }
-
- conn.execute(
- 'INSERT INTO "history" ("id", "type", "url", "data", "created_at") VALUES (?, ?, ?, ?, ?)',
- (
- "single-id",
- str(StoreType.HISTORY),
- item_data["url"],
- json.dumps(item_data),
- datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S"),
- ),
- )
- conn.commit()
-
- datastore = DataStore(type=StoreType.HISTORY, connection=conn)
-
- items, total, page, total_pages = datastore.get_items_paginated(page=1, per_page=10)
+ db = await make_db(data=1)
+ try:
+ datastore = DataStore(type=StoreType.HISTORY, connection=db)
+ items, total, page, total_pages = await datastore.get_items_paginated(page=1, per_page=10)
assert len(items) == 1
assert total == 1
assert page == 1
assert total_pages == 1
+ finally:
+ await db.close()
- conn.close()
-
- def test_pagination_status_filter_include(self, temp_db):
+ async def test_pagination_status_filter_include(self):
"""Test pagination with status filter (inclusion)."""
# Add some items with different status values
item_data_pending = {
@@ -269,50 +224,51 @@ class TestDataStorePagination:
"status": "downloading",
}
- temp_db.execute(
- 'INSERT INTO "history" ("id", "type", "url", "data", "created_at") VALUES (?, ?, ?, ?, ?)',
- (
- "pending-id",
- str(StoreType.HISTORY),
- item_data_pending["url"],
- json.dumps(item_data_pending),
- datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S"),
- ),
- )
- temp_db.execute(
- 'INSERT INTO "history" ("id", "type", "url", "data", "created_at") VALUES (?, ?, ?, ?, ?)',
- (
- "downloading-id",
- str(StoreType.HISTORY),
- item_data_downloading["url"],
- json.dumps(item_data_downloading),
- datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S"),
- ),
- )
- temp_db.commit()
+ db = await make_db(data=100)
+ try:
+ await db._conn.execute(
+ 'INSERT INTO "history" ("id", "type", "url", "data", "created_at") VALUES (?, ?, ?, ?, ?)',
+ (
+ "pending-id",
+ str(StoreType.HISTORY),
+ item_data_pending["url"],
+ json.dumps(item_data_pending),
+ datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S"),
+ ),
+ )
+ await db._conn.execute(
+ 'INSERT INTO "history" ("id", "type", "url", "data", "created_at") VALUES (?, ?, ?, ?, ?)',
+ (
+ "downloading-id",
+ str(StoreType.HISTORY),
+ item_data_downloading["url"],
+ json.dumps(item_data_downloading),
+ datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S"),
+ ),
+ )
+ await db._conn.commit()
+ datastore = DataStore(type=StoreType.HISTORY, connection=db)
- datastore = DataStore(type=StoreType.HISTORY, connection=temp_db)
+ # Filter for finished items only
+ items, total, _, __ = await datastore.get_items_paginated(page=1, per_page=50, status_filter="finished")
- # Filter for finished items only
- items, total, _page, _total_pages = datastore.get_items_paginated(
- page=1, per_page=50, status_filter="finished"
- )
+ assert len(items) == 50 # First page of finished items
+ assert total == 100 # Only 100 finished items in fixture
+ for _item_id, item in items:
+ assert item.info.status == "finished"
- assert len(items) == 50 # First page of finished items
- assert total == 100 # Only 100 finished items in fixture
- for _item_id, item in items:
- assert item.status == "finished"
+ # Filter for pending items only
+ items, total, _page, _total_pages = await datastore.get_items_paginated(
+ page=1, per_page=50, status_filter="pending"
+ )
- # Filter for pending items only
- items, total, _page, _total_pages = datastore.get_items_paginated(
- page=1, per_page=50, status_filter="pending"
- )
+ assert len(items) == 1
+ assert total == 1
+ assert items[0][1].info.status == "pending"
+ finally:
+ await db.close()
- assert len(items) == 1
- assert total == 1
- assert items[0][1].status == "pending"
-
- def test_pagination_status_filter_exclude(self, temp_db):
+ async def test_pagination_status_filter_exclude(self):
"""Test pagination with status filter (exclusion)."""
# Add some items with different status values
item_data_pending = {
@@ -329,55 +285,62 @@ class TestDataStorePagination:
"folder": "/downloads",
"status": "error",
}
+ db = await make_db(data=0)
+ try:
+ await db._conn.execute(
+ 'INSERT INTO "history" ("id", "type", "url", "data", "created_at") VALUES (?, ?, ?, ?, ?)',
+ (
+ "pending-id-2",
+ str(StoreType.HISTORY),
+ item_data_pending["url"],
+ json.dumps(item_data_pending),
+ datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S"),
+ ),
+ )
+ await db._conn.execute(
+ 'INSERT INTO "history" ("id", "type", "url", "data", "created_at") VALUES (?, ?, ?, ?, ?)',
+ (
+ "error-id",
+ str(StoreType.HISTORY),
+ item_data_error["url"],
+ json.dumps(item_data_error),
+ datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S"),
+ ),
+ )
+ await db._conn.commit()
- temp_db.execute(
- 'INSERT INTO "history" ("id", "type", "url", "data", "created_at") VALUES (?, ?, ?, ?, ?)',
- (
- "pending-id-2",
- str(StoreType.HISTORY),
- item_data_pending["url"],
- json.dumps(item_data_pending),
- datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S"),
- ),
- )
- temp_db.execute(
- 'INSERT INTO "history" ("id", "type", "url", "data", "created_at") VALUES (?, ?, ?, ?, ?)',
- (
- "error-id",
- str(StoreType.HISTORY),
- item_data_error["url"],
- json.dumps(item_data_error),
- datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S"),
- ),
- )
- temp_db.commit()
+ datastore = DataStore(type=StoreType.HISTORY, connection=db)
- datastore = DataStore(type=StoreType.HISTORY, connection=temp_db)
+ # Exclude finished items
+ items, total, _page, _total_pages = await datastore.get_items_paginated(
+ page=1, per_page=50, status_filter="!finished"
+ )
- # Exclude finished items
- items, total, _page, _total_pages = datastore.get_items_paginated(
- page=1, per_page=50, status_filter="!finished"
- )
+ assert total == 2 # Only 2 non-finished items
+ assert len(items) == 2
+ for _item_id, item in items:
+ assert item.info.status != "finished"
- assert total == 2 # Only 2 non-finished items
- assert len(items) == 2
- for _item_id, item in items:
- assert item.status != "finished"
+ # Verify we have pending and error
+ statuses = {item.info.status for _, item in items}
+ assert statuses == {"pending", "error"}
+ finally:
+ await db.close()
- # Verify we have pending and error
- statuses = {item.status for _, item in items}
- assert statuses == {"pending", "error"}
-
- def test_pagination_status_filter_none_matching(self, temp_db):
+ async def test_pagination_status_filter_none_matching(self):
"""Test pagination with status filter that matches no items."""
- datastore = DataStore(type=StoreType.HISTORY, connection=temp_db)
+ db = await make_db(data=0)
+ try:
+ datastore = DataStore(type=StoreType.HISTORY, connection=db)
- # Filter for a status that doesn't exist
- items, total, page, total_pages = datastore.get_items_paginated(
- page=1, per_page=50, status_filter="nonexistent"
- )
+ # Filter for a status that doesn't exist
+ items, total, page, total_pages = await datastore.get_items_paginated(
+ page=1, per_page=50, status_filter="nonexistent"
+ )
- assert len(items) == 0
- assert total == 0
- assert page == 1
- assert total_pages == 1
+ assert len(items) == 0
+ assert total == 0
+ assert page == 1
+ assert total_pages == 1
+ finally:
+ await db.close()
diff --git a/app/tests/test_sqlite_store.py b/app/tests/test_sqlite_store.py
new file mode 100644
index 00000000..fc0e36e9
--- /dev/null
+++ b/app/tests/test_sqlite_store.py
@@ -0,0 +1,256 @@
+from datetime import UTC, datetime, timedelta
+
+import pytest
+
+from app.library.ItemDTO import ItemDTO
+from app.library.operations import Operation
+from app.library.sqlite_store import SqliteStore
+
+
+async def make_store() -> SqliteStore:
+ """Create an isolated in-memory SqliteStore instance with schema."""
+ SqliteStore._reset_singleton()
+ store = SqliteStore.get_instance(db_path=":memory:")
+ await store._ensure_conn()
+ assert store._conn is not None
+ return store
+
+
+def make_item(idx: int, *, status: str = "finished") -> ItemDTO:
+ return ItemDTO(
+ id=f"vid{idx}",
+ title=f"Video {idx}",
+ url=f"https://example.com/{idx}",
+ folder="/downloads",
+ status=status,
+ )
+
+
+@pytest.mark.asyncio
+async def test_enqueue_upsert_and_fetch_saved():
+ store = await make_store()
+ item = make_item(1)
+
+ await store.enqueue_upsert("queue", item)
+ await store.flush()
+
+ saved = await store.fetch_saved("queue")
+ assert len(saved) == 1
+ key, loaded = saved[0]
+ assert key == item._id
+ assert loaded.title == item.title
+ await store.close()
+
+
+@pytest.mark.asyncio
+async def test_delete_and_bulk_delete():
+ store = await make_store()
+ items = [make_item(i) for i in range(3)]
+ for itm in items:
+ await store.enqueue_upsert("queue", itm)
+ await store.flush()
+
+ await store.enqueue_delete("queue", items[0]._id)
+ await store.enqueue_bulk_delete("queue", [items[1]._id])
+ await store.flush()
+
+ remaining = await store.fetch_saved("queue")
+ assert len(remaining) == 1
+ assert remaining[0][0] == items[2]._id
+ await store.close()
+
+
+@pytest.mark.asyncio
+async def test_count_with_status_filters():
+ store = await make_store()
+ finished_items = [make_item(i, status="finished") for i in range(2)]
+ pending_items = [make_item(i + 10, status="pending") for i in range(3)]
+
+ for itm in finished_items + pending_items:
+ await store.enqueue_upsert("history", itm)
+ await store.flush()
+
+ assert await store.count("history", status_filter="finished") == 2
+ assert await store.count("history", status_filter="pending") == 3
+ assert await store.count("history", status_filter="!finished") == 3
+ await store.close()
+
+
+@pytest.mark.asyncio
+async def test_paginate_order_and_bounds():
+ store = await make_store()
+ base = datetime(2024, 1, 1, tzinfo=UTC)
+ _list: list[ItemDTO] = []
+ for i in range(15):
+ itm = make_item(i)
+ encoded = itm.json()
+ created_at = (base + timedelta(minutes=i)).strftime("%Y-%m-%d %H:%M:%S")
+ _list.append(itm)
+ await store._conn.execute(
+ 'INSERT INTO "history" ("id", "type", "url", "data", "created_at") VALUES (?, ?, ?, ?, ?)',
+ (itm._id, "history", itm.url, encoded, created_at),
+ )
+ await store._conn.commit()
+
+ items, total, page, total_pages = await store.paginate("history", page=1, per_page=5, order="ASC")
+ assert total == 15
+ assert page == 1
+ assert total_pages == 3
+ assert items[0][0] == _list[0]._id
+
+ items_desc, *_ = await store.paginate("history", page=1, per_page=3, order="DESC")
+ assert len(items_desc) == 3
+ assert items_desc[0][0] == _list[14]._id
+ await store.close()
+
+@pytest.mark.asyncio
+async def test_get_by_id():
+ store = await make_store()
+ item = make_item(99)
+ await store.enqueue_upsert("queue", item)
+ await store.flush()
+ assert item._id == (await store.get_by_id("queue", item._id))._id
+ await store.close()
+
+@pytest.mark.asyncio
+async def test_shutdown_closes_worker_queue():
+ store = await make_store()
+ await store.enqueue_upsert("queue", make_item(99))
+ await store.shutdown()
+ assert store._queue is None
+ await store.close()
+
+
+@pytest.mark.asyncio
+async def test_enqueue_delete_removes_row():
+ store = await make_store()
+ item = make_item(5)
+ await store.enqueue_upsert("queue", item)
+ await store.flush()
+
+ await store.enqueue_delete("queue", item._id)
+ await store.flush()
+
+ rows = await store.fetch_saved("queue")
+ assert rows == []
+ await store.close()
+
+
+@pytest.mark.asyncio
+async def test_enqueue_bulk_delete_returns_count_and_bulk_path():
+ store = await make_store()
+ items = [make_item(i) for i in range(3)]
+ for itm in items:
+ await store.enqueue_upsert("history", itm)
+ await store.flush()
+
+ await store.enqueue_bulk_delete("history", [items[0]._id, items[1]._id])
+ await store.flush()
+
+ remaining = await store.fetch_saved("history")
+ assert len(remaining) == 1
+ assert remaining[0][0] == items[2]._id
+
+ # direct bulk_delete should report affected rows
+ count = await store.bulk_delete("history", [items[2]._id])
+ assert count == 1
+ await store.close()
+
+
+@pytest.mark.asyncio
+async def test_paginate_out_of_range_returns_last_page():
+ store = await make_store()
+ for i in range(7):
+ await store.enqueue_upsert("history", make_item(i))
+ await store.flush()
+
+ items, total, page, total_pages = await store.paginate("history", page=10, per_page=3, order="DESC")
+ assert total == 7
+ assert total_pages == 3
+ assert page == 3
+ assert len(items) == 1
+ await store.close()
+
+
+@pytest.mark.asyncio
+async def test_get_item_returns_none_without_kwargs():
+ store = await make_store()
+ await store.enqueue_upsert("queue", make_item(1))
+ await store.flush()
+
+ result = await store.get_item("queue")
+ assert result is None
+ await store.close()
+
+
+@pytest.mark.asyncio
+async def test_get_item_matches_conditions():
+ store = await make_store()
+ first = make_item(1, status="finished")
+ second = make_item(2, status="pending")
+
+ await store.enqueue_upsert("queue", first)
+ await store.enqueue_upsert("queue", second)
+ await store.flush()
+
+ # equality match
+ match_equal = await store.get_item("queue", title=first.title)
+ assert match_equal is not None
+ assert match_equal._id == first._id
+
+ # NOT_EQUAL should find the second item
+ match_not_equal = await store.get_item("queue", status=(Operation.NOT_EQUAL, "finished"))
+ assert match_not_equal is not None
+ assert match_not_equal._id == second._id
+
+ # CONTAIN should find first created (first)
+ match_contain = await store.get_item("queue", title=(Operation.CONTAIN, "Video"))
+ assert match_contain is not None
+ assert match_contain._id == first._id
+
+ # No match returns None
+ no_match = await store.get_item("queue", title=(Operation.CONTAIN, "does-not-exist"))
+ assert no_match is None
+ await store.close()
+
+@pytest.mark.asyncio
+async def test_on_shutdown_closes_connection():
+ store = await make_store()
+ await store.enqueue_upsert("queue", make_item(1))
+ await store.flush()
+
+ await store.on_shutdown(None)
+ assert store._conn is None
+
+
+@pytest.mark.asyncio
+async def test_exists_and_get_by_key_and_url():
+ store = await make_store()
+ item = make_item(42)
+ await store.enqueue_upsert("queue", item)
+ await store.flush()
+
+ assert await store.exists("queue", key=item._id) is True
+ assert await store.exists("queue", url=item.url) is True
+
+ fetched_by_key = await store.get("queue", key=item._id)
+ assert fetched_by_key is not None
+ assert fetched_by_key._id == item._id
+
+ fetched_by_url = await store.get("queue", url=item.url)
+ assert fetched_by_url is not None
+ assert fetched_by_url._id == item._id
+
+ assert await store.exists("queue", key="missing") is False
+ assert await store.get("queue", key="missing") is None
+ await store.close()
+
+
+@pytest.mark.asyncio
+async def test_exists_and_get_raise_without_key_or_url():
+ store = await make_store()
+ with pytest.raises(KeyError):
+ await store.exists("queue")
+ with pytest.raises(KeyError):
+ await store.get("queue")
+ await store.close()
diff --git a/pyproject.toml b/pyproject.toml
index 5fe7565d..dd6482b9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -15,7 +15,6 @@ dependencies = [
# Cross‐platform
"python-socketio>=5.11.1",
"aiohttp>=3.9.3",
- "caribou>=0.3.0",
"coloredlogs>=15.0.1",
"aiocron>=1.8",
"python-dotenv>=1.0.1",
@@ -23,7 +22,7 @@ dependencies = [
"httpx[brotli,http2,socks,zstd]",
"async-timeout",
"pyjson5",
- "curl_cffi>=0.13",
+ "curl_cffi==0.13",
"pysubs2",
"regex",
"mutagen",
@@ -44,6 +43,7 @@ dependencies = [
"parsel>=1.10.0",
"jmespath>=1.0.1",
"jsonschema>=4.23.0",
+ "aiosqlite>=0.22.0",
]
[tool.ruff]
diff --git a/ui/app/components/History.vue b/ui/app/components/History.vue
index a5141d9e..ebf9be3a 100644
--- a/ui/app/components/History.vue
+++ b/ui/app/components/History.vue
@@ -271,17 +271,17 @@
-
pImg(e)" :src="getImage(item)" v-if="getImage(item)" />
+
-
pImg(e)" :src="getImage(item)" v-if="getImage(item)" />
+
-
pImg(e)" v-if="getImage(item)" :src="getImage(item)" />
+
@@ -847,6 +847,14 @@ const pImg = (e: Event) => {
}
}
+const onImgError = (e: Event) => {
+ const target = e.target as HTMLImageElement
+ if (target.src.endsWith('/images/placeholder.png')) {
+ return
+ }
+ target.src = '/images/placeholder.png'
+}
+
watch(video_item, (v) => {
if (!bg_enable.value) {
return
diff --git a/ui/app/components/Queue.vue b/ui/app/components/Queue.vue
index c376a0eb..d2aafa29 100644
--- a/ui/app/components/Queue.vue
+++ b/ui/app/components/Queue.vue
@@ -193,13 +193,13 @@
-
pImg(e)"
+
-
pImg(e)" v-if="item.extras?.thumbnail"
+
@@ -579,6 +579,14 @@ const pImg = (e: Event) => {
}
}
+const onImgError = (e: Event) => {
+ const target = e.target as HTMLImageElement
+ if (target.src.endsWith('/images/placeholder.png')) {
+ return
+ }
+ target.src = '/images/placeholder.png'
+}
+
watch(embed_url, v => {
if (!bg_enable.value) {
return
diff --git a/ui/app/components/Simple.vue b/ui/app/components/Simple.vue
index b78a711c..dcc790fb 100644
--- a/ui/app/components/Simple.vue
+++ b/ui/app/components/Simple.vue
@@ -89,7 +89,8 @@
-
+
{{ getDurationLabel(entry.item) }}
@@ -688,18 +689,19 @@ const loadMoreHistory = async (): Promise => {
}
}
-// Setup intersection observer for infinite scroll
-useIntersectionObserver(
- loadMoreTrigger,
- ([entry]) => {
- if (entry?.isIntersecting && !paginationInfo.value.isLoading && paginationInfo.value.page < paginationInfo.value.total_pages) {
- loadMoreHistory()
- }
- },
- {
- threshold: 0.5,
+const onImgError = (e: Event) => {
+ const target = e.target as HTMLImageElement
+ if (target.src.endsWith('/images/placeholder.png')) {
+ return
}
-)
+ target.src = '/images/placeholder.png'
+}
+
+useIntersectionObserver(loadMoreTrigger, ([entry]) => {
+ if (entry?.isIntersecting && !paginationInfo.value.isLoading && paginationInfo.value.page < paginationInfo.value.total_pages) {
+ loadMoreHistory()
+ }
+}, { threshold: 0.5 })
diff --git a/ui/app/stores/SocketStore.ts b/ui/app/stores/SocketStore.ts
index bb7dfc81..bfc517c4 100644
--- a/ui/app/stores/SocketStore.ts
+++ b/ui/app/stores/SocketStore.ts
@@ -118,10 +118,6 @@ export const useSocketStore = defineStore('socket', () => {
config.add('folders', json.data.folders)
}
- if (json.data?.queue) {
- stateStore.addAll('queue', json.data.queue || {})
- }
-
if (typeof json.data?.history_count === 'number') {
stateStore.setHistoryCount(json.data.history_count)
}
@@ -129,6 +125,14 @@ export const useSocketStore = defineStore('socket', () => {
error.value = null;
})
+ on('active_queue', stream => {
+ const json = JSON.parse(stream);
+ if (!json?.data?.queue) {
+ return;
+ }
+ stateStore.addAll('queue', json.data.queue || {})
+ })
+
on('item_added', stream => {
const json = JSON.parse(stream);
stateStore.add('queue', json.data._id, json.data);
diff --git a/ui/nuxt.config.ts b/ui/nuxt.config.ts
index 92dc4f60..c7d24573 100644
--- a/ui/nuxt.config.ts
+++ b/ui/nuxt.config.ts
@@ -68,7 +68,7 @@ export default defineNuxtConfig({
'@pinia/nuxt',
'@vueuse/nuxt',
'floating-vue/nuxt',
- process.env.NODE_ENV === 'development' ? '@nuxt/eslint' : '',
+ 'development' === process.env.NODE_ENV ? '@nuxt/eslint' : '',
].filter(Boolean),
nitro: {
@@ -80,6 +80,9 @@ export default defineNuxtConfig({
vite: {
server: {
allowedHosts: true,
+ },
+ build: {
+ chunkSizeWarningLimit: 2000,
}
},
telemetry: false,
diff --git a/ui/package.json b/ui/package.json
index 8476214d..5126839a 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -32,11 +32,11 @@
"marked-base-url": "^1.1.8",
"marked-gfm-heading-id": "^4.1.3",
"moment": "^2.30.1",
- "nuxt": "^4.2.1",
+ "nuxt": "^4.2.2",
"pinia": "^3.0.4",
"socket.io-client": "^4.8.1",
"vue": "^3.5.25",
- "vue-router": "^4.6.3",
+ "vue-router": "^4.6.4",
"vue-toastification": "2.0.0-rc.5"
},
"pnpm": {
@@ -52,13 +52,13 @@
"ansi-regex": "6.2.0"
},
"devDependencies": {
- "@nuxt/eslint": "^1.11.0",
- "@nuxt/eslint-config": "^1.11.0",
- "@typescript-eslint/parser": "^8.48.1",
- "eslint": "^9.39.1",
+ "@nuxt/eslint": "^1.12.1",
+ "@nuxt/eslint-config": "^1.12.1",
+ "@typescript-eslint/parser": "^8.50.0",
+ "eslint": "^9.39.2",
"typescript": "^5.9.3",
- "vitest": "^4.0.15",
+ "vitest": "^4.0.16",
"vue-eslint-parser": "^10.2.0",
- "vue-tsc": "^3.1.5"
+ "vue-tsc": "^3.1.8"
}
}
diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml
index 45dc7890..5650f476 100644
--- a/ui/pnpm-lock.yaml
+++ b/ui/pnpm-lock.yaml
@@ -16,7 +16,7 @@ importers:
version: 14.1.0(vue@3.5.25(typescript@5.9.3))
'@vueuse/nuxt':
specifier: ^14.1.0
- version: 14.1.0(magicast@0.5.1)(nuxt@4.2.1(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.25)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.3)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.5(typescript@5.9.3))(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))
+ version: 14.1.0(magicast@0.5.1)(nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.25)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.5)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.8(typescript@5.9.3))(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))
'@xterm/addon-fit':
specifier: ^0.10.0
version: 0.10.0(@xterm/xterm@5.5.0)
@@ -31,7 +31,7 @@ importers:
version: 3.9.0
floating-vue:
specifier: ^5.2.2
- version: 5.2.2(@nuxt/kit@3.20.1(magicast@0.5.1))(vue@3.5.25(typescript@5.9.3))
+ version: 5.2.2(@nuxt/kit@3.20.2(magicast@0.5.1))(vue@3.5.25(typescript@5.9.3))
hls.js:
specifier: ^1.6.15
version: 1.6.15
@@ -51,8 +51,8 @@ importers:
specifier: ^2.30.1
version: 2.30.1
nuxt:
- specifier: ^4.2.1
- version: 4.2.1(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.25)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.3)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.5(typescript@5.9.3))(yaml@2.8.2)
+ specifier: ^4.2.2
+ version: 4.2.2(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.25)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.5)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.8(typescript@5.9.3))(yaml@2.8.2)
pinia:
specifier: ^3.0.4
version: 3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
@@ -63,36 +63,36 @@ importers:
specifier: ^3.5.25
version: 3.5.25(typescript@5.9.3)
vue-router:
- specifier: ^4.6.3
- version: 4.6.3(vue@3.5.25(typescript@5.9.3))
+ specifier: ^4.6.4
+ version: 4.6.4(vue@3.5.25(typescript@5.9.3))
vue-toastification:
specifier: 2.0.0-rc.5
version: 2.0.0-rc.5(vue@3.5.25(typescript@5.9.3))
devDependencies:
'@nuxt/eslint':
- specifier: ^1.11.0
- version: 1.11.0(@typescript-eslint/utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.25)(eslint@9.39.1(jiti@2.6.1))(magicast@0.5.1)(typescript@5.9.3)(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))
+ specifier: ^1.12.1
+ version: 1.12.1(@typescript-eslint/utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.25)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.1)(typescript@5.9.3)(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))
'@nuxt/eslint-config':
- specifier: ^1.11.0
- version: 1.11.0(@typescript-eslint/utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.25)(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
+ specifier: ^1.12.1
+ version: 1.12.1(@typescript-eslint/utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.25)(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/parser':
- specifier: ^8.48.1
- version: 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
+ specifier: ^8.50.0
+ version: 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
eslint:
- specifier: ^9.39.1
- version: 9.39.1(jiti@2.6.1)
+ specifier: ^9.39.2
+ version: 9.39.2(jiti@2.6.1)
typescript:
specifier: ^5.9.3
version: 5.9.3
vitest:
- specifier: ^4.0.15
- version: 4.0.15(@types/node@22.18.1)(jiti@2.6.1)(jsdom@27.0.0)(terser@5.44.1)(yaml@2.8.2)
+ specifier: ^4.0.16
+ version: 4.0.16(@types/node@22.18.1)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(terser@5.44.1)(yaml@2.8.2)
vue-eslint-parser:
specifier: ^10.2.0
- version: 10.2.0(eslint@9.39.1(jiti@2.6.1))
+ version: 10.2.0(eslint@9.39.2(jiti@2.6.1))
vue-tsc:
- specifier: ^3.1.5
- version: 3.1.5(typescript@5.9.3)
+ specifier: ^3.1.8
+ version: 3.1.8(typescript@5.9.3)
packages:
@@ -105,11 +105,11 @@ packages:
peerDependencies:
'@types/json-schema': ^7.0.15
- '@asamuzakjp/css-color@4.1.0':
- resolution: {integrity: sha512-9xiBAtLn4aNsa4mDnpovJvBn72tNEIACyvlqaNJ+ADemR+yeMJWnBudOi2qGDviJa7SwcDOU/TRh5dnET7qk0w==}
+ '@asamuzakjp/css-color@4.1.1':
+ resolution: {integrity: sha512-B0Hv6G3gWGMn0xKJ0txEi/jM5iFpT3MfDxmhZFb4W047GvytCf1DHQ1D69W3zHI4yWe2aTZAA0JnbMZ7Xc8DuQ==}
- '@asamuzakjp/dom-selector@6.7.5':
- resolution: {integrity: sha512-Eks6dY8zau4m4wNRQjRVaKQRTalNcPcBvU1ZQ35w5kKRk1gUeNCkVLsRiATurjASTp3TKM4H10wsI50nx3NZdw==}
+ '@asamuzakjp/dom-selector@6.7.6':
+ resolution: {integrity: sha512-hBaJER6A9MpdG3WgdlOolHmbOYvSk46y7IQN/1+iqiCuUu6iWdQrs9DGKF8ocqsEqWujWf/V7b7vaDgiUmIvUg==}
'@asamuzakjp/nwsapi@2.3.9':
resolution: {integrity: sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==}
@@ -286,9 +286,11 @@ packages:
peerDependencies:
'@csstools/css-tokenizer': ^3.0.4
- '@csstools/css-syntax-patches-for-csstree@1.0.20':
- resolution: {integrity: sha512-8BHsjXfSciZxjmHQOuVdW2b8WLUPts9a+mfL13/PzEviufUEW2xnvQuOlKs9dRBHgRqJ53SF/DUoK9+MZk72oQ==}
+ '@csstools/css-syntax-patches-for-csstree@1.0.14':
+ resolution: {integrity: sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==}
engines: {node: '>=18'}
+ peerDependencies:
+ postcss: ^8.4
'@csstools/css-tokenizer@3.0.4':
resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==}
@@ -670,8 +672,8 @@ packages:
resolution: {integrity: sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@eslint/js@9.39.1':
- resolution: {integrity: sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==}
+ '@eslint/js@9.39.2':
+ resolution: {integrity: sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@eslint/object-schema@2.1.7':
@@ -774,8 +776,8 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
- '@nuxt/cli@3.31.1':
- resolution: {integrity: sha512-2Quw4bQlVpMGS/AD34KUEsd4i5PVz993e//km10fYR3AKiilYCHiY+vYdvU9odtFYzmr3tQtfZb1rFfb3GUiCQ==}
+ '@nuxt/cli@3.31.2':
+ resolution: {integrity: sha512-ud4KcfSdPeY96IR3UCtg/k7p6nUbJqF3IguQsolHo6EEJwiNM283EFXhRzU9cR+1iILExjaJvHMpFJ/7Xi++bg==}
engines: {node: ^16.10.0 || >=18.0.0}
hasBin: true
@@ -801,8 +803,8 @@ packages:
'@vitejs/devtools':
optional: true
- '@nuxt/eslint-config@1.11.0':
- resolution: {integrity: sha512-SIe05zqUzwp/3mLW1AhUtCPdLVKjpNDO2ODxniReb5lsuRzSp7q9OQhjUhC1QqEM2ANH3q3b8Qui9YpxUg3nMA==}
+ '@nuxt/eslint-config@1.12.1':
+ resolution: {integrity: sha512-fsKKtIIvVwQ5OGE30lJEhzwXxXj40ol7vR6h3eTH8sSBVZLOdmPn2BHrhoOjHTDXpLPw1AZ/8GcQfJZ2o3gcHQ==}
peerDependencies:
eslint: ^9.0.0
eslint-plugin-format: '*'
@@ -810,13 +812,13 @@ packages:
eslint-plugin-format:
optional: true
- '@nuxt/eslint-plugin@1.11.0':
- resolution: {integrity: sha512-kTNwHJjdr5qW90ww4ct8kFKdWEVb/8X4KdkDJAmRTRqLRfkfTQDOKDfpQa62fRnsuRopMEPjab3Bsya4YLbb9g==}
+ '@nuxt/eslint-plugin@1.12.1':
+ resolution: {integrity: sha512-9EBWZTgJC2oclDIL53YG6paEoaTU2SDWVPybEQ0Pe2Bm/5YSbHd//6EGLvdGwAgN+xJQmEsPunUpd4Y+NX2OCQ==}
peerDependencies:
eslint: ^9.0.0
- '@nuxt/eslint@1.11.0':
- resolution: {integrity: sha512-N3jq7AL2rsalVoLs0hKFjba9kmfQ8yx2KwAxCzHwq0hhaFOhwnoTfEMJo8JcKyQ+DCbWy1f38jfBk5RJhKadBg==}
+ '@nuxt/eslint@1.12.1':
+ resolution: {integrity: sha512-weXMt09C2XsWo7mpkVciApTXXaNUYQ1IbvrURNtnhpJcvcb2WkQutIOc/+pIhTsmb2O3T1t23HL76+Ll+7bpFQ==}
peerDependencies:
eslint: ^9.0.0
eslint-webpack-plugin: ^4.1.0
@@ -827,22 +829,22 @@ packages:
vite-plugin-eslint2:
optional: true
- '@nuxt/kit@3.20.1':
- resolution: {integrity: sha512-TIslaylfI5kd3AxX5qts0qyrIQ9Uq3HAA1bgIIJ+c+zpDfK338YS+YrCWxBBzDMECRCbAS58mqAd2MtJfG1ENA==}
+ '@nuxt/kit@3.20.2':
+ resolution: {integrity: sha512-laqfmMcWWNV1FsVmm1+RQUoGY8NIJvCRl0z0K8ikqPukoEry0LXMqlQ+xaf8xJRvoH2/78OhZmsEEsUBTXipcw==}
engines: {node: '>=18.12.0'}
- '@nuxt/kit@4.2.1':
- resolution: {integrity: sha512-lLt8KLHyl7IClc3RqRpRikz15eCfTRlAWL9leVzPyg5N87FfKE/7EWgWvpiL/z4Tf3dQCIqQb88TmHE0JTIDvA==}
+ '@nuxt/kit@4.2.2':
+ resolution: {integrity: sha512-ZAgYBrPz/yhVgDznBNdQj2vhmOp31haJbO0I0iah/P9atw+OHH7NJLUZ3PK+LOz/0fblKTN1XJVSi8YQ1TQ0KA==}
engines: {node: '>=18.12.0'}
- '@nuxt/nitro-server@4.2.1':
- resolution: {integrity: sha512-P6zGvKgbjwDO28A4QnRuhL0riNSxcw317nGSYfP9Z+V+GyCNVc9yCcAEuzRIvm+dv4PB6VC708my8Jq30VM9Ow==}
+ '@nuxt/nitro-server@4.2.2':
+ resolution: {integrity: sha512-lDITf4n5bHQ6a5MO7pvkpdQbPdWAUgSvztSHCfui/3ioLZsM2XntlN02ue6GSoh3oV9H4xSB3qGa+qlSjgxN0A==}
engines: {node: ^20.19.0 || >=22.12.0}
peerDependencies:
- nuxt: ^4.2.1
+ nuxt: ^4.2.2
- '@nuxt/schema@4.2.1':
- resolution: {integrity: sha512-kSuma7UztDVyw8eAmN3rKFoaWjNRkJE9+kqwEurpuxG7nCwFPS7sUPSGzovzaofP+xV30tl6wveBEcDRWyQvgA==}
+ '@nuxt/schema@4.2.2':
+ resolution: {integrity: sha512-lW/1MNpO01r5eR/VoeanQio8Lg4QpDklMOHa4mBHhhPNlBO1qiRtVYzjcnNdun3hujGauRaO9khGjv93Z5TZZA==}
engines: {node: ^14.18.0 || >=16.10.0}
'@nuxt/telemetry@2.6.6':
@@ -850,301 +852,301 @@ packages:
engines: {node: '>=18.12.0'}
hasBin: true
- '@nuxt/vite-builder@4.2.1':
- resolution: {integrity: sha512-SuBxCtGrHcbgrtzHwJgLe0pBXWw2T9RFQx9JQ7A3dE9RjBhzjaxtmjVHx7vtq6DCGi0d0WlW1Z1lBZUDaXy8WA==}
+ '@nuxt/vite-builder@4.2.2':
+ resolution: {integrity: sha512-Bot8fpJNtHZrM4cS1iSR7bEAZ1mFLAtJvD/JOSQ6kT62F4hSFWfMubMXOwDkLK2tnn3bnAdSqGy1nLNDBCahpQ==}
engines: {node: ^20.19.0 || >=22.12.0}
peerDependencies:
- nuxt: 4.2.1
+ nuxt: 4.2.2
rolldown: ^1.0.0-beta.38
vue: ^3.3.4
peerDependenciesMeta:
rolldown:
optional: true
- '@oxc-minify/binding-android-arm64@0.96.0':
- resolution: {integrity: sha512-lzeIEMu/v6Y+La5JSesq4hvyKtKBq84cgQpKYTYM/yGuNk2tfd5Ha31hnC+mTh48lp/5vZH+WBfjVUjjINCfug==}
+ '@oxc-minify/binding-android-arm64@0.102.0':
+ resolution: {integrity: sha512-pknM+ttJTwRr7ezn1v5K+o2P4RRjLAzKI10bjVDPybwWQ544AZW6jxm7/YDgF2yUbWEV9o7cAQPkIUOmCiW8vg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
- '@oxc-minify/binding-darwin-arm64@0.96.0':
- resolution: {integrity: sha512-i0LkJAUXb4BeBFrJQbMKQPoxf8+cFEffDyLSb7NEzzKuPcH8qrVsnEItoOzeAdYam8Sr6qCHVwmBNEQzl7PWpw==}
+ '@oxc-minify/binding-darwin-arm64@0.102.0':
+ resolution: {integrity: sha512-BDLiH41ZctNND38+GCEL3ZxFn9j7qMZJLrr6SLWMt8xlG4Sl64xTkZ0zeUy4RdVEatKKZdrRIhFZ2e5wPDQT6Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
- '@oxc-minify/binding-darwin-x64@0.96.0':
- resolution: {integrity: sha512-C5vI0WPR+KPIFAD5LMOJk2J8iiT+Nv65vDXmemzXEXouzfEOLYNqnW+u6NSsccpuZHHWAiLyPFkYvKFduveAUQ==}
+ '@oxc-minify/binding-darwin-x64@0.102.0':
+ resolution: {integrity: sha512-AcB8ZZ711w4hTDhMfMHNjT2d+hekTQ2XmNSUBqJdXB+a2bJbE50UCRq/nxXl44zkjaQTit3lcQbFvhk2wwKcpw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
- '@oxc-minify/binding-freebsd-x64@0.96.0':
- resolution: {integrity: sha512-3//5DNx+xUjVBMLLk2sl6hfe4fwfENJtjVQUBXjxzwPuv8xgZUqASG4cRG3WqG5Qe8dV6SbCI4EgKQFjO4KCZA==}
+ '@oxc-minify/binding-freebsd-x64@0.102.0':
+ resolution: {integrity: sha512-UlLEN9mR5QaviYVMWZQsN9DgAH3qyV67XUXDEzSrbVMLsqHsVHhFU8ZIeO0fxWTQW/cgpvldvKp9/+RdrggqWw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
- '@oxc-minify/binding-linux-arm-gnueabihf@0.96.0':
- resolution: {integrity: sha512-WXChFKV7VdDk1NePDK1J31cpSvxACAVztJ7f7lJVYBTkH+iz5D0lCqPcE7a9eb7nC3xvz4yk7DM6dA9wlUQkQg==}
+ '@oxc-minify/binding-linux-arm-gnueabihf@0.102.0':
+ resolution: {integrity: sha512-CWyCwedZrUt47n56/RwHSwKXxVI3p98hB0ntLaBNeH5qjjBujs9uOh4bQ0aAlzUWunT77b3/Y+xcQnmV42HN4A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
- '@oxc-minify/binding-linux-arm-musleabihf@0.96.0':
- resolution: {integrity: sha512-7B18glYMX4Z/YoqgE3VRLs/2YhVLxlxNKSgrtsRpuR8xv58xca+hEhiFwZN1Rn+NSMZ29Z33LWD7iYWnqYFvRA==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [arm]
- os: [linux]
-
- '@oxc-minify/binding-linux-arm64-gnu@0.96.0':
- resolution: {integrity: sha512-Yl+KcTldsEJNcaYxxonwAXZ2q3gxIzn3kXYQWgKWdaGIpNhOCWqF+KE5WLsldoh5Ro5SHtomvb8GM6cXrIBMog==}
+ '@oxc-minify/binding-linux-arm64-gnu@0.102.0':
+ resolution: {integrity: sha512-W/DCw+Ys8rXj4j38ylJ2l6Kvp6SV+eO5SUWA11imz7yCWntNL001KJyGQ9PJNUFHg0jbxe3yqm4M50v6miWzeA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@oxc-minify/binding-linux-arm64-musl@0.96.0':
- resolution: {integrity: sha512-rNqoFWOWaxwMmUY5fspd/h5HfvgUlA3sv9CUdA2MpnHFiyoJNovR7WU8tGh+Yn0qOAs0SNH0a05gIthHig14IA==}
+ '@oxc-minify/binding-linux-arm64-musl@0.102.0':
+ resolution: {integrity: sha512-DyH/t/zSZHuX4Nn239oBteeMC4OP7B13EyXWX18Qg8aJoZ+lZo90WPGOvhP04zII33jJ7di+vrtAUhsX64lp+A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@oxc-minify/binding-linux-riscv64-gnu@0.96.0':
- resolution: {integrity: sha512-3paajIuzGnukHwSI3YBjYVqbd72pZd8NJxaayaNFR0AByIm8rmIT5RqFXbq8j2uhtpmNdZRXiu0em1zOmIScWA==}
+ '@oxc-minify/binding-linux-riscv64-gnu@0.102.0':
+ resolution: {integrity: sha512-CMvzrmOg+Gs44E7TRK/IgrHYp+wwVJxVV8niUrDR2b3SsrCO3NQz5LI+7bM1qDbWnuu5Cl1aiitoMfjRY61dSg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
- '@oxc-minify/binding-linux-s390x-gnu@0.96.0':
- resolution: {integrity: sha512-9ESrpkB2XG0lQ89JlsxlZa86iQCOs+jkDZLl6O+u5wb7ynUy21bpJJ1joauCOSYIOUlSy3+LbtJLiqi7oSQt5Q==}
+ '@oxc-minify/binding-linux-s390x-gnu@0.102.0':
+ resolution: {integrity: sha512-tZWr6j2s0ddm9MTfWTI3myaAArg9GDy4UgvpF00kMQAjLcGUNhEEQbB9Bd9KtCvDQzaan8HQs0GVWUp+DWrymw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
- '@oxc-minify/binding-linux-x64-gnu@0.96.0':
- resolution: {integrity: sha512-UMM1jkns+p+WwwmdjC5giI3SfR2BCTga18x3C0cAu6vDVf4W37uTZeTtSIGmwatTBbgiq++Te24/DE0oCdm1iQ==}
+ '@oxc-minify/binding-linux-x64-gnu@0.102.0':
+ resolution: {integrity: sha512-0YEKmAIun1bS+Iy5Shx6WOTSj3GuilVuctJjc5/vP8/EMTZ/RI8j0eq0Mu3UFPoT/bMULL3MBXuHuEIXmq7Ddg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@oxc-minify/binding-linux-x64-musl@0.96.0':
- resolution: {integrity: sha512-8b1naiC7MdP7xeMi7cQ5tb9W1rZAP9Qz/jBRqp1Y5EOZ1yhSGnf1QWuZ/0pCc+XiB9vEHXEY3Aki/H+86m2eOg==}
+ '@oxc-minify/binding-linux-x64-musl@0.102.0':
+ resolution: {integrity: sha512-Ew4QDpEsXoV+pG5+bJpheEy3GH436GBe6ASPB0X27Hh9cQ2gb1NVZ7cY7xJj68+fizwS/PtT8GHoG3uxyH17Pg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
- '@oxc-minify/binding-wasm32-wasi@0.96.0':
- resolution: {integrity: sha512-bjGDjkGzo3GWU9Vg2qiFUrfoo5QxojPNV/2RHTlbIB5FWkkV4ExVjsfyqihFiAuj0NXIZqd2SAiEq9htVd3RFw==}
+ '@oxc-minify/binding-openharmony-arm64@0.102.0':
+ resolution: {integrity: sha512-wYPXS8IOu/sXiP3CGHJNPzZo4hfPAwJKevcFH2syvU2zyqUxym7hx6smfcK/mgJBiX7VchwArdGRwrEQKcBSaQ==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@oxc-minify/binding-wasm32-wasi@0.102.0':
+ resolution: {integrity: sha512-52SepCb9e+8cVisGa9S/F14K8PxW0AnbV1j4KEYi8uwfkUIxeDNKRHVHzPoBXNrr0yxW0EHLn/3i8J7a2YCpWw==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
- '@oxc-minify/binding-win32-arm64-msvc@0.96.0':
- resolution: {integrity: sha512-4L4DlHUT47qMWQuTyUghpncR3NZHWtxvd0G1KgSjVgXf+cXzFdWQCWZZtCU0yrmOoVCNUf4S04IFCJyAe+Ie7A==}
+ '@oxc-minify/binding-win32-arm64-msvc@0.102.0':
+ resolution: {integrity: sha512-kLs6H1y6sDBKcIimkNwu5th28SLkyvFpHNxdLtCChda0KIGeIXNSiupy5BqEutY+VlWJivKT1OV3Ev3KC5Euzg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
- '@oxc-minify/binding-win32-x64-msvc@0.96.0':
- resolution: {integrity: sha512-T2ijfqZLpV2bgGGocXV4SXTuMoouqN0asYTIm+7jVOLvT5XgDogf3ZvCmiEnSWmxl21+r5wHcs8voU2iUROXAg==}
+ '@oxc-minify/binding-win32-x64-msvc@0.102.0':
+ resolution: {integrity: sha512-XdyJZdSMN8rbBXH10CrFuU+Q9jIP2+MnxHmNzjK4+bldbTI1UxqwjUMS9bKVC5VCaIEZhh8IE8x4Vf8gmCgrKQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
- '@oxc-parser/binding-android-arm64@0.96.0':
- resolution: {integrity: sha512-CofbPOiW1PG+hi8bgElJPK0ioHfw8nt4Vw9d+Q9JuMhygS6LbQyu1W6tIFZ1OPFofeFRdWus3vD29FBx+tvFOA==}
+ '@oxc-parser/binding-android-arm64@0.102.0':
+ resolution: {integrity: sha512-pD2if3w3cxPvYbsBSTbhxAYGDaG6WVwnqYG0mYRQ142D6SJ6BpNs7YVQrqpRA2AJQCmzaPP5TRp/koFLebagfQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
- '@oxc-parser/binding-darwin-arm64@0.96.0':
- resolution: {integrity: sha512-+HZ2L1a/1BsUXYik8XqQwT2Tl5Z3jRQ/RRQiPV9UsB2skKyd91NLDlQlMpdhjLGs9Qe7Y42unFjRg2iHjIiwnw==}
+ '@oxc-parser/binding-darwin-arm64@0.102.0':
+ resolution: {integrity: sha512-RzMN6f6MrjjpQC2Dandyod3iOscofYBpHaTecmoRRbC5sJMwsurkqUMHzoJX9F6IM87kn8m/JcClnoOfx5Sesw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
- '@oxc-parser/binding-darwin-x64@0.96.0':
- resolution: {integrity: sha512-GC8wH1W0XaCLyTeGsmyaMdnItiYQkqfTcn9Ygc55AWI+m11lCjQeoKDIsDCm/QwrKLCN07u3WWWsuPs5ubfXpA==}
+ '@oxc-parser/binding-darwin-x64@0.102.0':
+ resolution: {integrity: sha512-Sr2/3K6GEcejY+HgWp5HaxRPzW5XHe9IfGKVn9OhLt8fzVLnXbK5/GjXj7JjMCNKI3G3ZPZDG2Dgm6CX3MaHCA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
- '@oxc-parser/binding-freebsd-x64@0.96.0':
- resolution: {integrity: sha512-8SeXi2FmlN15uPY5oM03cua5RXBDYmY34Uewongv6RUiAaU/kWxLvzuijpyNC+yQ1r4fC2LbWJhAsKpX5qkA6g==}
+ '@oxc-parser/binding-freebsd-x64@0.102.0':
+ resolution: {integrity: sha512-s9F2N0KJCGEpuBW6ChpFfR06m2Id9ReaHSl8DCca4HvFNt8SJFPp8fq42n2PZy68rtkremQasM0JDrK2BoBeBQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
- '@oxc-parser/binding-linux-arm-gnueabihf@0.96.0':
- resolution: {integrity: sha512-UEs+Zf6T2/FwQlLgv7gfZsKmY19sl3hK57r2BQVc2eCmCmF/deeqDcWyFjzkNLgdDDucY60PoNhNGClDm605uQ==}
+ '@oxc-parser/binding-linux-arm-gnueabihf@0.102.0':
+ resolution: {integrity: sha512-zRCIOWzLbqhfY4g8KIZDyYfO2Fl5ltxdQI1v2GlePj66vFWRl8cf4qcBGzxKfsH3wCZHAhmWd1Ht59mnrfH/UQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
- '@oxc-parser/binding-linux-arm-musleabihf@0.96.0':
- resolution: {integrity: sha512-1kuWvjR2+ORJMoyxt9LSbLcDhXZnL25XOuv9VmH6NmSPvLgewzuubSlm++W03x+U7SzWFilBsdwIHtD/0mjERw==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [arm]
- os: [linux]
-
- '@oxc-parser/binding-linux-arm64-gnu@0.96.0':
- resolution: {integrity: sha512-PHH4ETR1t0fymxuhpQNj3Z9t/78/zZa2Lj3Z3I0ZOd+/Ex+gtdhGoB5xYyy7lcYGAPMfZ+Gmr+dTCr1GYNZ3BA==}
+ '@oxc-parser/binding-linux-arm64-gnu@0.102.0':
+ resolution: {integrity: sha512-5n5RbHgfjulRhKB0pW5p0X/NkQeOpI4uI9WHgIZbORUDATGFC8yeyPA6xYGEs+S3MyEAFxl4v544UEIWwqAgsA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@oxc-parser/binding-linux-arm64-musl@0.96.0':
- resolution: {integrity: sha512-fjDPbZjkqaDSTBe0FM8nZ9zBw4B/NF/I0gH7CfvNDwIj9smISaNFypYeomkvubORpnbX9ORhvhYwg3TxQ60OGA==}
+ '@oxc-parser/binding-linux-arm64-musl@0.102.0':
+ resolution: {integrity: sha512-/XWcmglH/VJ4yKAGTLRgPKSSikh3xciNxkwGiURt8dS30b+3pwc4ZZmudMu0tQ3mjSu0o7V9APZLMpbHK8Bp5w==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@oxc-parser/binding-linux-riscv64-gnu@0.96.0':
- resolution: {integrity: sha512-59KAHd/6/LmjkdSAuJn0piKmwSavMasWNUKuYLX/UnqI5KkGIp14+LBwwaBG6KzOtIq1NrRCnmlL4XSEaNkzTg==}
+ '@oxc-parser/binding-linux-riscv64-gnu@0.102.0':
+ resolution: {integrity: sha512-2jtIq4nswvy6xdqv1ndWyvVlaRpS0yqomLCvvHdCFx3pFXo5Aoq4RZ39kgvFWrbAtpeYSYeAGFnwgnqjx9ftdw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
- '@oxc-parser/binding-linux-s390x-gnu@0.96.0':
- resolution: {integrity: sha512-VtupojtgahY8XmLwpVpM3C1WQEgMD1JxpB8lzUtdSLwosWaaz1EAl+VXWNuxTTZusNuLBtmR+F0qql22ISi/9g==}
+ '@oxc-parser/binding-linux-s390x-gnu@0.102.0':
+ resolution: {integrity: sha512-Yp6HX/574mvYryiqj0jNvNTJqo4pdAsNP2LPBTxlDQ1cU3lPd7DUA4MQZadaeLI8+AGB2Pn50mPuPyEwFIxeFg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
- '@oxc-parser/binding-linux-x64-gnu@0.96.0':
- resolution: {integrity: sha512-8XSY9aUYY+5I4I1mhSEWmYqdUrJi3J5cCAInvEVHyTnDAPkhb+tnLGVZD696TpW+lFOLrTFF2V5GMWJVafqIUA==}
+ '@oxc-parser/binding-linux-x64-gnu@0.102.0':
+ resolution: {integrity: sha512-R4b0xZpDRhoNB2XZy0kLTSYm0ZmWeKjTii9fcv1Mk3/SIGPrrglwt4U6zEtwK54Dfi4Bve5JnQYduigR/gyDzw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@oxc-parser/binding-linux-x64-musl@0.96.0':
- resolution: {integrity: sha512-IIVNtqhA0uxKkD8Y6aZinKO/sOD5O62VlduE54FnUU2rzZEszrZQLL8nMGVZhTdPaKW5M1aeLmjcdnOs6er1Jg==}
+ '@oxc-parser/binding-linux-x64-musl@0.102.0':
+ resolution: {integrity: sha512-xM5A+03Ti3jvWYZoqaBRS3lusvnvIQjA46Fc9aBE/MHgvKgHSkrGEluLWg/33QEwBwxupkH25Pxc1yu97oZCtg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
- '@oxc-parser/binding-wasm32-wasi@0.96.0':
- resolution: {integrity: sha512-TJ/sNPbVD4u6kUwm7sDKa5iRDEB8vd7ZIMjYqFrrAo9US1RGYOSvt6Ie9sDRekUL9fZhNsykvSrpmIj6dg/C2w==}
+ '@oxc-parser/binding-openharmony-arm64@0.102.0':
+ resolution: {integrity: sha512-AieLlsliblyaTFq7Iw9Nc618tgwV02JT4fQ6VIUd/3ZzbluHIHfPjIXa6Sds+04krw5TvCS8lsegtDYAyzcyhg==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@oxc-parser/binding-wasm32-wasi@0.102.0':
+ resolution: {integrity: sha512-w6HRyArs1PBb9rDsQSHlooe31buUlUI2iY8sBzp62jZ1tmvaJo9EIVTQlRNDkwJmk9DF9uEyIJ82EkZcCZTs9A==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
- '@oxc-parser/binding-win32-arm64-msvc@0.96.0':
- resolution: {integrity: sha512-zCOhRB7MYVIHLj+2QYoTuLyaipiD8JG/ggUjfsMUaupRPpvwQNhsxINLIcTcb0AS+OsT7/OREhydjO74STqQzQ==}
+ '@oxc-parser/binding-win32-arm64-msvc@0.102.0':
+ resolution: {integrity: sha512-pqP5UuLiiFONQxqGiUFMdsfybaK1EOK4AXiPlvOvacLaatSEPObZGpyCkAcj9aZcvvNwYdeY9cxGM9IT3togaA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
- '@oxc-parser/binding-win32-x64-msvc@0.96.0':
- resolution: {integrity: sha512-J6zfx9TE0oS+TrqBUjMVMOi/d/j3HMj69Pip263pETOEPm788N0HXKPsc2X2jUfSTHzD9vmdjq0QFymbf2LhWg==}
+ '@oxc-parser/binding-win32-x64-msvc@0.102.0':
+ resolution: {integrity: sha512-ntMcL35wuLR1A145rLSmm7m7j8JBZGkROoB9Du0KFIFcfi/w1qk75BdCeiTl3HAKrreAnuhW3QOGs6mJhntowA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
- '@oxc-project/types@0.96.0':
- resolution: {integrity: sha512-r/xkmoXA0xEpU6UGtn18CNVjXH6erU3KCpCDbpLmbVxBFor1U9MqN5Z2uMmCHJuXjJzlnDR+hWY+yPoLo8oHDw==}
+ '@oxc-project/types@0.102.0':
+ resolution: {integrity: sha512-8Skrw405g+/UJPKWJ1twIk3BIH2nXdiVlVNtYT23AXVwpsd79es4K+KYt06Fbnkc5BaTvk/COT2JuCLYdwnCdA==}
- '@oxc-transform/binding-android-arm64@0.96.0':
- resolution: {integrity: sha512-wOm+ZsqFvyZ7B9RefUMsj0zcXw77Z2pXA51nbSQyPXqr+g0/pDGxriZWP8Sdpz/e4AEaKPA9DvrwyOZxu7GRDQ==}
+ '@oxc-transform/binding-android-arm64@0.102.0':
+ resolution: {integrity: sha512-JLBT7EiExsGmB6LuBBnm6qTfg0rLSxBU+F7xjqy6UXYpL7zhqelGJL7IAq6Pu5UYFT55zVlXXmgzLOXQfpQjXA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [android]
- '@oxc-transform/binding-darwin-arm64@0.96.0':
- resolution: {integrity: sha512-td1sbcvzsyuoNRiNdIRodPXRtFFwxzPpC/6/yIUtRRhKn30XQcizxupIvQQVpJWWchxkphbBDh6UN+u+2CJ8Zw==}
+ '@oxc-transform/binding-darwin-arm64@0.102.0':
+ resolution: {integrity: sha512-xmsBCk/NwE0khy8h6wLEexiS5abCp1ZqJUNHsAovJdGgIW21oGwhiC3VYg1vNLbq+zEXwOHuphVuNEYfBwyNTw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [darwin]
- '@oxc-transform/binding-darwin-x64@0.96.0':
- resolution: {integrity: sha512-xgqxnqhPYH2NYkgbqtnCJfhbXvxIf/pnhF/ig5UBK8PYpCEWIP/cfLpQRQ9DcQnRfuxi7RMIF6LdmB1AiS6Fkg==}
+ '@oxc-transform/binding-darwin-x64@0.102.0':
+ resolution: {integrity: sha512-EhBsiq8hSd5BRjlWACB9MxTUiZT2He1s1b3tRP8k3lB8ZTt6sXnDXIWhxRmmM0h//xe6IJ2HuMlbvjXPo/tATg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [darwin]
- '@oxc-transform/binding-freebsd-x64@0.96.0':
- resolution: {integrity: sha512-1i67OXdl/rvSkcTXqDlh6qGRXYseEmf0rl/R+/i88scZ/o3A+FzlX56sThuaPzSSv9eVgesnoYUjIBJELFc1oA==}
+ '@oxc-transform/binding-freebsd-x64@0.102.0':
+ resolution: {integrity: sha512-eujvuYf0x7BFgKyFecbXUa2JBEXT4Ss6vmyrrhVdN07jaeJRiobaKAmeNXBkanoWL2KQLELJbSBgs1ykWYTkzg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [freebsd]
- '@oxc-transform/binding-linux-arm-gnueabihf@0.96.0':
- resolution: {integrity: sha512-9MJBs0SWODsqyzO3eAnacXgJ/sZu1xqinjEwBzkcZ3tQI8nKhMADOzu2NzbVWDWujeoC8DESXaO08tujvUru+Q==}
+ '@oxc-transform/binding-linux-arm-gnueabihf@0.102.0':
+ resolution: {integrity: sha512-2x7Ro356PHBVp1SS/dOsHBSnrfs5MlPYwhdKg35t6qixt2bv1kzEH0tDmn4TNEbdjOirmvOXoCTEWUvh8A4f4Q==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm]
os: [linux]
- '@oxc-transform/binding-linux-arm-musleabihf@0.96.0':
- resolution: {integrity: sha512-BQom57I2ScccixljNYh2Wy+5oVZtF1LXiiUPxSLtDHbsanpEvV/+kzCagQpTjk1BVzSQzOxfEUWjvL7mY53pRQ==}
- engines: {node: ^20.19.0 || >=22.12.0}
- cpu: [arm]
- os: [linux]
-
- '@oxc-transform/binding-linux-arm64-gnu@0.96.0':
- resolution: {integrity: sha512-kaqvUzNu8LL4aBSXqcqGVLFG13GmJEplRI2+yqzkgAItxoP/LfFMdEIErlTWLGyBwd0OLiNMHrOvkcCQRWadVg==}
+ '@oxc-transform/binding-linux-arm64-gnu@0.102.0':
+ resolution: {integrity: sha512-Rz/RbPvT4QwcHKIQ/cOt6Lwl4c7AhK2b6whZfyL6oJ7Uz8UiVl1BCwk8thedrB5h+FEykmaPHoriW1hmBev60g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@oxc-transform/binding-linux-arm64-musl@0.96.0':
- resolution: {integrity: sha512-EiG/L3wEkPgTm4p906ufptyblBgtiQWTubGg/JEw82f8uLRroayr5zhbUqx40EgH037a3SfJthIyLZi7XPRFJw==}
+ '@oxc-transform/binding-linux-arm64-musl@0.102.0':
+ resolution: {integrity: sha512-I08iWABrN7zakn3wuNIBWY3hALQGsDLPQbZT1mXws7tyiQqJNGe49uS0/O50QhX3KXj+mbRGsmjVXLXGJE1CVQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@oxc-transform/binding-linux-riscv64-gnu@0.96.0':
- resolution: {integrity: sha512-r01CY6OxKGtVeYnvH4mGmtkQMlLkXdPWWNXwo5o7fE2s/fgZPMpqh8bAuXEhuMXipZRJrjxTk1+ZQ4KCHpMn3Q==}
+ '@oxc-transform/binding-linux-riscv64-gnu@0.102.0':
+ resolution: {integrity: sha512-9+SYW1ARAF6Oj/82ayoqKRe8SI7O1qvzs3Y0kijvhIqAaaZWcFRjI5DToyWRAbnzTtHlMcSllZLXNYdmxBjFxA==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [riscv64]
os: [linux]
libc: [glibc]
- '@oxc-transform/binding-linux-s390x-gnu@0.96.0':
- resolution: {integrity: sha512-4djg2vYLGbVeS8YiA2K4RPPpZE4fxTGCX5g/bOMbCYyirDbmBAIop4eOAj8vOA9i1CcWbDtmp+PVJ1dSw7f3IQ==}
+ '@oxc-transform/binding-linux-s390x-gnu@0.102.0':
+ resolution: {integrity: sha512-HV9nTyQw0TTKYPu+gBhaJBioomiM9O4LcGXi+s5IylCGG6imP0/U13q/9xJnP267QFmiWWqnnSFcv0QAWCyh8A==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [s390x]
os: [linux]
libc: [glibc]
- '@oxc-transform/binding-linux-x64-gnu@0.96.0':
- resolution: {integrity: sha512-f6pcWVz57Y8jXa2OS7cz3aRNuks34Q3j61+3nQ4xTE8H1KbalcEvHNmM92OEddaJ8QLs9YcE0kUC6eDTbY34+A==}
+ '@oxc-transform/binding-linux-x64-gnu@0.102.0':
+ resolution: {integrity: sha512-4wcZ08mmdFk8OjsnglyeYGu5PW3TDh87AmcMOi7tZJ3cpJjfzwDfY27KTEUx6G880OpjAiF36OFSPwdKTKgp2g==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@oxc-transform/binding-linux-x64-musl@0.96.0':
- resolution: {integrity: sha512-NSiRtFvR7Pbhv3mWyPMkTK38czIjcnK0+K5STo3CuzZRVbX1TM17zGdHzKBUHZu7v6IQ6/XsQ3ELa1BlEHPGWQ==}
+ '@oxc-transform/binding-linux-x64-musl@0.102.0':
+ resolution: {integrity: sha512-rUHZSZBw0FUnUgOhL/Rs7xJz9KjH2eFur/0df6Lwq/isgJc/ggtBtFoZ+y4Fb8ON87a3Y2gS2LT7SEctX0XdPQ==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [linux]
libc: [musl]
- '@oxc-transform/binding-wasm32-wasi@0.96.0':
- resolution: {integrity: sha512-A91ARLiuZHGN4hBds9s7bW3czUuLuHLsV+cz44iF9j8e1zX9m2hNGXf/acQRbg/zcFUXmjz5nmk8EkZyob876w==}
+ '@oxc-transform/binding-openharmony-arm64@0.102.0':
+ resolution: {integrity: sha512-98y4tccTQ/pA+r2KA/MEJIZ7J8TNTJ4aCT4rX8kWK4pGOko2YsfY3Ru9DVHlLDwmVj7wP8Z4JNxdBrAXRvK+0g==}
+ engines: {node: ^20.19.0 || >=22.12.0}
+ cpu: [arm64]
+ os: [openharmony]
+
+ '@oxc-transform/binding-wasm32-wasi@0.102.0':
+ resolution: {integrity: sha512-M6myOXxHty3L2TJEB1NlJPtQm0c0LmivAxcGv/+DSDadOoB/UnOUbjM8W2Utlh5IYS9ARSOjqHtBiPYLWJ15XA==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
- '@oxc-transform/binding-win32-arm64-msvc@0.96.0':
- resolution: {integrity: sha512-IedJf40djKgDObomhYjdRAlmSYUEdfqX3A3M9KfUltl9AghTBBLkTzUMA7O09oo71vYf5TEhbFM7+Vn5vqw7AQ==}
+ '@oxc-transform/binding-win32-arm64-msvc@0.102.0':
+ resolution: {integrity: sha512-jzaA1lLiMXiJs4r7E0BHRxTPiwAkpoCfSNRr8npK/SqL4UQE4cSz3WDTX5wJWRrN2U+xqsDGefeYzH4reI8sgw==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [arm64]
os: [win32]
- '@oxc-transform/binding-win32-x64-msvc@0.96.0':
- resolution: {integrity: sha512-0fI0P0W7bSO/GCP/N5dkmtB9vBqCA4ggo1WmXTnxNJVmFFOtcA1vYm1I9jl8fxo+sucW2WnlpnI4fjKdo3JKxA==}
+ '@oxc-transform/binding-win32-x64-msvc@0.102.0':
+ resolution: {integrity: sha512-eYOm6mch+1cP9qlNkMdorfBFY8aEOxY/isqrreLmEWqF/hyXA0SbLKDigTbvh3JFKny/gXlHoCKckqfua4cwtg==}
engines: {node: ^20.19.0 || >=22.12.0}
cpu: [x64]
os: [win32]
@@ -1255,21 +1257,21 @@ packages:
'@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
- '@poppinss/colors@4.1.5':
- resolution: {integrity: sha512-FvdDqtcRCtz6hThExcFOgW0cWX+xwSMWcRuQe5ZEb2m7cVQOAVZOIMt+/v9RxGiD9/OY16qJBXK4CVKWAPalBw==}
+ '@poppinss/colors@4.1.6':
+ resolution: {integrity: sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg==}
'@poppinss/dumper@0.6.5':
resolution: {integrity: sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw==}
- '@poppinss/exception@1.2.2':
- resolution: {integrity: sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==}
-
- '@rolldown/pluginutils@1.0.0-beta.50':
- resolution: {integrity: sha512-5e76wQiQVeL1ICOZVUg4LSOVYg9jyhGCin+icYozhsUzM+fHE7kddi1bdiE0jwVqTfkjba3jUFbEkoC9WkdvyA==}
+ '@poppinss/exception@1.2.3':
+ resolution: {integrity: sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw==}
'@rolldown/pluginutils@1.0.0-beta.53':
resolution: {integrity: sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==}
+ '@rolldown/pluginutils@1.0.0-beta.9-commit.d91dfb5':
+ resolution: {integrity: sha512-8sExkWRK+zVybw3+2/kBkYBFeLnEUWz1fT7BLHplpzmtqkOfTbAQ9gkt4pzwGIIZmg4Qn5US5ACjUBenrhezwQ==}
+
'@rollup/plugin-alias@5.1.1':
resolution: {integrity: sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==}
engines: {node: '>=14.0.0'}
@@ -1342,124 +1344,124 @@ packages:
rollup:
optional: true
- '@rollup/rollup-android-arm-eabi@4.53.3':
- resolution: {integrity: sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==}
+ '@rollup/rollup-android-arm-eabi@4.53.5':
+ resolution: {integrity: sha512-iDGS/h7D8t7tvZ1t6+WPK04KD0MwzLZrG0se1hzBjSi5fyxlsiggoJHwh18PCFNn7tG43OWb6pdZ6Y+rMlmyNQ==}
cpu: [arm]
os: [android]
- '@rollup/rollup-android-arm64@4.53.3':
- resolution: {integrity: sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==}
+ '@rollup/rollup-android-arm64@4.53.5':
+ resolution: {integrity: sha512-wrSAViWvZHBMMlWk6EJhvg8/rjxzyEhEdgfMMjREHEq11EtJ6IP6yfcCH57YAEca2Oe3FNCE9DSTgU70EIGmVw==}
cpu: [arm64]
os: [android]
- '@rollup/rollup-darwin-arm64@4.53.3':
- resolution: {integrity: sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==}
+ '@rollup/rollup-darwin-arm64@4.53.5':
+ resolution: {integrity: sha512-S87zZPBmRO6u1YXQLwpveZm4JfPpAa6oHBX7/ghSiGH3rz/KDgAu1rKdGutV+WUI6tKDMbaBJomhnT30Y2t4VQ==}
cpu: [arm64]
os: [darwin]
- '@rollup/rollup-darwin-x64@4.53.3':
- resolution: {integrity: sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==}
+ '@rollup/rollup-darwin-x64@4.53.5':
+ resolution: {integrity: sha512-YTbnsAaHo6VrAczISxgpTva8EkfQus0VPEVJCEaboHtZRIb6h6j0BNxRBOwnDciFTZLDPW5r+ZBmhL/+YpTZgA==}
cpu: [x64]
os: [darwin]
- '@rollup/rollup-freebsd-arm64@4.53.3':
- resolution: {integrity: sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==}
+ '@rollup/rollup-freebsd-arm64@4.53.5':
+ resolution: {integrity: sha512-1T8eY2J8rKJWzaznV7zedfdhD1BqVs1iqILhmHDq/bqCUZsrMt+j8VCTHhP0vdfbHK3e1IQ7VYx3jlKqwlf+vw==}
cpu: [arm64]
os: [freebsd]
- '@rollup/rollup-freebsd-x64@4.53.3':
- resolution: {integrity: sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==}
+ '@rollup/rollup-freebsd-x64@4.53.5':
+ resolution: {integrity: sha512-sHTiuXyBJApxRn+VFMaw1U+Qsz4kcNlxQ742snICYPrY+DDL8/ZbaC4DVIB7vgZmp3jiDaKA0WpBdP0aqPJoBQ==}
cpu: [x64]
os: [freebsd]
- '@rollup/rollup-linux-arm-gnueabihf@4.53.3':
- resolution: {integrity: sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==}
+ '@rollup/rollup-linux-arm-gnueabihf@4.53.5':
+ resolution: {integrity: sha512-dV3T9MyAf0w8zPVLVBptVlzaXxka6xg1f16VAQmjg+4KMSTWDvhimI/Y6mp8oHwNrmnmVl9XxJ/w/mO4uIQONA==}
cpu: [arm]
os: [linux]
libc: [glibc]
- '@rollup/rollup-linux-arm-musleabihf@4.53.3':
- resolution: {integrity: sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==}
+ '@rollup/rollup-linux-arm-musleabihf@4.53.5':
+ resolution: {integrity: sha512-wIGYC1x/hyjP+KAu9+ewDI+fi5XSNiUi9Bvg6KGAh2TsNMA3tSEs+Sh6jJ/r4BV/bx/CyWu2ue9kDnIdRyafcQ==}
cpu: [arm]
os: [linux]
libc: [musl]
- '@rollup/rollup-linux-arm64-gnu@4.53.3':
- resolution: {integrity: sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==}
+ '@rollup/rollup-linux-arm64-gnu@4.53.5':
+ resolution: {integrity: sha512-Y+qVA0D9d0y2FRNiG9oM3Hut/DgODZbU9I8pLLPwAsU0tUKZ49cyV1tzmB/qRbSzGvY8lpgGkJuMyuhH7Ma+Vg==}
cpu: [arm64]
os: [linux]
libc: [glibc]
- '@rollup/rollup-linux-arm64-musl@4.53.3':
- resolution: {integrity: sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==}
+ '@rollup/rollup-linux-arm64-musl@4.53.5':
+ resolution: {integrity: sha512-juaC4bEgJsyFVfqhtGLz8mbopaWD+WeSOYr5E16y+1of6KQjc0BpwZLuxkClqY1i8sco+MdyoXPNiCkQou09+g==}
cpu: [arm64]
os: [linux]
libc: [musl]
- '@rollup/rollup-linux-loong64-gnu@4.53.3':
- resolution: {integrity: sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==}
+ '@rollup/rollup-linux-loong64-gnu@4.53.5':
+ resolution: {integrity: sha512-rIEC0hZ17A42iXtHX+EPJVL/CakHo+tT7W0pbzdAGuWOt2jxDFh7A/lRhsNHBcqL4T36+UiAgwO8pbmn3dE8wA==}
cpu: [loong64]
os: [linux]
libc: [glibc]
- '@rollup/rollup-linux-ppc64-gnu@4.53.3':
- resolution: {integrity: sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==}
+ '@rollup/rollup-linux-ppc64-gnu@4.53.5':
+ resolution: {integrity: sha512-T7l409NhUE552RcAOcmJHj3xyZ2h7vMWzcwQI0hvn5tqHh3oSoclf9WgTl+0QqffWFG8MEVZZP1/OBglKZx52Q==}
cpu: [ppc64]
os: [linux]
libc: [glibc]
- '@rollup/rollup-linux-riscv64-gnu@4.53.3':
- resolution: {integrity: sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==}
+ '@rollup/rollup-linux-riscv64-gnu@4.53.5':
+ resolution: {integrity: sha512-7OK5/GhxbnrMcxIFoYfhV/TkknarkYC1hqUw1wU2xUN3TVRLNT5FmBv4KkheSG2xZ6IEbRAhTooTV2+R5Tk0lQ==}
cpu: [riscv64]
os: [linux]
libc: [glibc]
- '@rollup/rollup-linux-riscv64-musl@4.53.3':
- resolution: {integrity: sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==}
+ '@rollup/rollup-linux-riscv64-musl@4.53.5':
+ resolution: {integrity: sha512-GwuDBE/PsXaTa76lO5eLJTyr2k8QkPipAyOrs4V/KJufHCZBJ495VCGJol35grx9xryk4V+2zd3Ri+3v7NPh+w==}
cpu: [riscv64]
os: [linux]
libc: [musl]
- '@rollup/rollup-linux-s390x-gnu@4.53.3':
- resolution: {integrity: sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==}
+ '@rollup/rollup-linux-s390x-gnu@4.53.5':
+ resolution: {integrity: sha512-IAE1Ziyr1qNfnmiQLHBURAD+eh/zH1pIeJjeShleII7Vj8kyEm2PF77o+lf3WTHDpNJcu4IXJxNO0Zluro8bOw==}
cpu: [s390x]
os: [linux]
libc: [glibc]
- '@rollup/rollup-linux-x64-gnu@4.53.3':
- resolution: {integrity: sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==}
+ '@rollup/rollup-linux-x64-gnu@4.53.5':
+ resolution: {integrity: sha512-Pg6E+oP7GvZ4XwgRJBuSXZjcqpIW3yCBhK4BcsANvb47qMvAbCjR6E+1a/U2WXz1JJxp9/4Dno3/iSJLcm5auw==}
cpu: [x64]
os: [linux]
libc: [glibc]
- '@rollup/rollup-linux-x64-musl@4.53.3':
- resolution: {integrity: sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==}
+ '@rollup/rollup-linux-x64-musl@4.53.5':
+ resolution: {integrity: sha512-txGtluxDKTxaMDzUduGP0wdfng24y1rygUMnmlUJ88fzCCULCLn7oE5kb2+tRB+MWq1QDZT6ObT5RrR8HFRKqg==}
cpu: [x64]
os: [linux]
libc: [musl]
- '@rollup/rollup-openharmony-arm64@4.53.3':
- resolution: {integrity: sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==}
+ '@rollup/rollup-openharmony-arm64@4.53.5':
+ resolution: {integrity: sha512-3DFiLPnTxiOQV993fMc+KO8zXHTcIjgaInrqlG8zDp1TlhYl6WgrOHuJkJQ6M8zHEcntSJsUp1XFZSY8C1DYbg==}
cpu: [arm64]
os: [openharmony]
- '@rollup/rollup-win32-arm64-msvc@4.53.3':
- resolution: {integrity: sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==}
+ '@rollup/rollup-win32-arm64-msvc@4.53.5':
+ resolution: {integrity: sha512-nggc/wPpNTgjGg75hu+Q/3i32R00Lq1B6N1DO7MCU340MRKL3WZJMjA9U4K4gzy3dkZPXm9E1Nc81FItBVGRlA==}
cpu: [arm64]
os: [win32]
- '@rollup/rollup-win32-ia32-msvc@4.53.3':
- resolution: {integrity: sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==}
+ '@rollup/rollup-win32-ia32-msvc@4.53.5':
+ resolution: {integrity: sha512-U/54pTbdQpPLBdEzCT6NBCFAfSZMvmjr0twhnD9f4EIvlm9wy3jjQ38yQj1AGznrNO65EWQMgm/QUjuIVrYF9w==}
cpu: [ia32]
os: [win32]
- '@rollup/rollup-win32-x64-gnu@4.53.3':
- resolution: {integrity: sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==}
+ '@rollup/rollup-win32-x64-gnu@4.53.5':
+ resolution: {integrity: sha512-2NqKgZSuLH9SXBBV2dWNRCZmocgSOx8OJSdpRaEcRlIfX8YrKxUT6z0F1NpvDVhOsl190UFTRh2F2WDWWCYp3A==}
cpu: [x64]
os: [win32]
- '@rollup/rollup-win32-x64-msvc@4.53.3':
- resolution: {integrity: sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==}
+ '@rollup/rollup-win32-x64-msvc@4.53.5':
+ resolution: {integrity: sha512-JRpZUhCfhZ4keB5v0fe02gQJy05GqboPOaxvjugW04RLSYYoB/9t2lx2u/tMs/Na/1NXfY8QYjgRljRpN+MjTQ==}
cpu: [x64]
os: [win32]
@@ -1481,8 +1483,8 @@ packages:
'@speed-highlight/core@1.2.12':
resolution: {integrity: sha512-uilwrK0Ygyri5dToHYdZSjcvpS2ZwX0w5aSt3GCEN9hrjxWCoeV4Z2DTXuxjwbntaLQIEEAlCeNQss5SoHvAEA==}
- '@standard-schema/spec@1.0.0':
- resolution: {integrity: sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==}
+ '@standard-schema/spec@1.1.0':
+ resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==}
'@stylistic/eslint-plugin@5.6.1':
resolution: {integrity: sha512-JCs+MqoXfXrRPGbGmho/zGS/jMcn3ieKl/A8YImqib76C8kjgZwq5uUFzc30lJkMvcchuRn6/v8IApLxli3Jyw==}
@@ -1518,63 +1520,63 @@ packages:
'@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
- '@typescript-eslint/eslint-plugin@8.48.1':
- resolution: {integrity: sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==}
+ '@typescript-eslint/eslint-plugin@8.50.0':
+ resolution: {integrity: sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
- '@typescript-eslint/parser': ^8.48.1
+ '@typescript-eslint/parser': ^8.50.0
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/parser@8.48.1':
- resolution: {integrity: sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==}
+ '@typescript-eslint/parser@8.50.0':
+ resolution: {integrity: sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/project-service@8.48.1':
- resolution: {integrity: sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==}
+ '@typescript-eslint/project-service@8.50.0':
+ resolution: {integrity: sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/scope-manager@8.48.1':
- resolution: {integrity: sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==}
+ '@typescript-eslint/scope-manager@8.50.0':
+ resolution: {integrity: sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/tsconfig-utils@8.48.1':
- resolution: {integrity: sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==}
+ '@typescript-eslint/tsconfig-utils@8.50.0':
+ resolution: {integrity: sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/type-utils@8.48.1':
- resolution: {integrity: sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==}
+ '@typescript-eslint/type-utils@8.50.0':
+ resolution: {integrity: sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/types@8.48.1':
- resolution: {integrity: sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==}
+ '@typescript-eslint/types@8.50.0':
+ resolution: {integrity: sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- '@typescript-eslint/typescript-estree@8.48.1':
- resolution: {integrity: sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==}
+ '@typescript-eslint/typescript-estree@8.50.0':
+ resolution: {integrity: sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/utils@8.48.1':
- resolution: {integrity: sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==}
+ '@typescript-eslint/utils@8.50.0':
+ resolution: {integrity: sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
- '@typescript-eslint/visitor-keys@8.48.1':
- resolution: {integrity: sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==}
+ '@typescript-eslint/visitor-keys@8.50.0':
+ resolution: {integrity: sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@unhead/vue@2.0.19':
@@ -1697,18 +1699,18 @@ packages:
vite: ^5.0.0 || ^6.0.0 || ^7.0.0
vue: ^3.0.0
- '@vitejs/plugin-vue@6.0.2':
- resolution: {integrity: sha512-iHmwV3QcVGGvSC1BG5bZ4z6iwa1SOpAPWmnjOErd4Ske+lZua5K9TtAVdx0gMBClJ28DViCbSmZitjWZsWO3LA==}
+ '@vitejs/plugin-vue@6.0.3':
+ resolution: {integrity: sha512-TlGPkLFLVOY3T7fZrwdvKpjprR3s4fxRln0ORDo1VQ7HHyxJwTlrjKU3kpVWTlaAjIEuCTokmjkZnr8Tpc925w==}
engines: {node: ^20.19.0 || >=22.12.0}
peerDependencies:
- vite: ^5.0.0 || ^6.0.0 || ^7.0.0
+ vite: ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0
vue: ^3.2.25
- '@vitest/expect@4.0.15':
- resolution: {integrity: sha512-Gfyva9/GxPAWXIWjyGDli9O+waHDC0Q0jaLdFP1qPAUUfo1FEXPXUfUkp3eZA0sSq340vPycSyOlYUeM15Ft1w==}
+ '@vitest/expect@4.0.16':
+ resolution: {integrity: sha512-eshqULT2It7McaJkQGLkPjPjNph+uevROGuIMJdG3V+0BSR2w9u6J9Lwu+E8cK5TETlfou8GRijhafIMhXsimA==}
- '@vitest/mocker@4.0.15':
- resolution: {integrity: sha512-CZ28GLfOEIFkvCFngN8Sfx5h+Se0zN+h4B7yOsPVCcgtiO7t5jt9xQh2E1UkFep+eb9fjyMfuC5gBypwb07fvQ==}
+ '@vitest/mocker@4.0.16':
+ resolution: {integrity: sha512-yb6k4AZxJTB+q9ycAvsoxGn+j/po0UaPgajllBgt1PzoMAAmJGYFdDk0uCcRcxb3BrME34I6u8gHZTQlkqSZpg==}
peerDependencies:
msw: ^2.4.9
vite: ^6.0.0 || ^7.0.0-0
@@ -1718,29 +1720,29 @@ packages:
vite:
optional: true
- '@vitest/pretty-format@4.0.15':
- resolution: {integrity: sha512-SWdqR8vEv83WtZcrfLNqlqeQXlQLh2iilO1Wk1gv4eiHKjEzvgHb2OVc3mIPyhZE6F+CtfYjNlDJwP5MN6Km7A==}
+ '@vitest/pretty-format@4.0.16':
+ resolution: {integrity: sha512-eNCYNsSty9xJKi/UdVD8Ou16alu7AYiS2fCPRs0b1OdhJiV89buAXQLpTbe+X8V9L6qrs9CqyvU7OaAopJYPsA==}
- '@vitest/runner@4.0.15':
- resolution: {integrity: sha512-+A+yMY8dGixUhHmNdPUxOh0la6uVzun86vAbuMT3hIDxMrAOmn5ILBHm8ajrqHE0t8R9T1dGnde1A5DTnmi3qw==}
+ '@vitest/runner@4.0.16':
+ resolution: {integrity: sha512-VWEDm5Wv9xEo80ctjORcTQRJ539EGPB3Pb9ApvVRAY1U/WkHXmmYISqU5E79uCwcW7xYUV38gwZD+RV755fu3Q==}
- '@vitest/snapshot@4.0.15':
- resolution: {integrity: sha512-A7Ob8EdFZJIBjLjeO0DZF4lqR6U7Ydi5/5LIZ0xcI+23lYlsYJAfGn8PrIWTYdZQRNnSRlzhg0zyGu37mVdy5g==}
+ '@vitest/snapshot@4.0.16':
+ resolution: {integrity: sha512-sf6NcrYhYBsSYefxnry+DR8n3UV4xWZwWxYbCJUt2YdvtqzSPR7VfGrY0zsv090DAbjFZsi7ZaMi1KnSRyK1XA==}
- '@vitest/spy@4.0.15':
- resolution: {integrity: sha512-+EIjOJmnY6mIfdXtE/bnozKEvTC4Uczg19yeZ2vtCz5Yyb0QQ31QWVQ8hswJ3Ysx/K2EqaNsVanjr//2+P3FHw==}
+ '@vitest/spy@4.0.16':
+ resolution: {integrity: sha512-4jIOWjKP0ZUaEmJm00E0cOBLU+5WE0BpeNr3XN6TEF05ltro6NJqHWxXD0kA8/Zc8Nh23AT8WQxwNG+WeROupw==}
- '@vitest/utils@4.0.15':
- resolution: {integrity: sha512-HXjPW2w5dxhTD0dLwtYHDnelK3j8sR8cWIaLxr22evTyY6q8pRCjZSmhRWVjBaOVXChQd6AwMzi9pucorXCPZA==}
+ '@vitest/utils@4.0.16':
+ resolution: {integrity: sha512-h8z9yYhV3e1LEfaQ3zdypIrnAg/9hguReGZoS7Gl0aBG5xgA410zBqECqmaF/+RkTggRsfnzc1XaAHA6bmUufA==}
- '@volar/language-core@2.4.23':
- resolution: {integrity: sha512-hEEd5ET/oSmBC6pi1j6NaNYRWoAiDhINbT8rmwtINugR39loROSlufGdYMF9TaKGfz+ViGs1Idi3mAhnuPcoGQ==}
+ '@volar/language-core@2.4.26':
+ resolution: {integrity: sha512-hH0SMitMxnB43OZpyF1IFPS9bgb2I3bpCh76m2WEK7BE0A0EzpYsRp0CCH2xNKshr7kacU5TQBLYn4zj7CG60A==}
- '@volar/source-map@2.4.23':
- resolution: {integrity: sha512-Z1Uc8IB57Lm6k7q6KIDu/p+JWtf3xsXJqAX/5r18hYOTpJyBn0KXUR8oTJ4WFYOcDzWC9n3IflGgHowx6U6z9Q==}
+ '@volar/source-map@2.4.26':
+ resolution: {integrity: sha512-JJw0Tt/kSFsIRmgTQF4JSt81AUSI1aEye5Zl65EeZ8H35JHnTvFGmpDOBn5iOxd48fyGE+ZvZBp5FcgAy/1Qhw==}
- '@volar/typescript@2.4.23':
- resolution: {integrity: sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==}
+ '@volar/typescript@2.4.26':
+ resolution: {integrity: sha512-N87ecLD48Sp6zV9zID/5yuS1+5foj0DfuYGdQ6KHj/IbKvyKv1zNX6VCmnKYwtmHadEO6mFc2EKISiu3RDPAvA==}
'@vue-macros/common@3.1.1':
resolution: {integrity: sha512-afW2DMjgCBVs33mWRlz7YsGHzoEEupnl0DK5ZTKsgziAlLh5syc5m+GM7eqeYrgiQpwMaVxa1fk73caCvPxyAw==}
@@ -1802,8 +1804,8 @@ packages:
'@vue/devtools-shared@8.0.5':
resolution: {integrity: sha512-bRLn6/spxpmgLk+iwOrR29KrYnJjG9DGpHGkDFG82UM21ZpJ39ztUT9OXX3g+usW7/b2z+h46I9ZiYyB07XMXg==}
- '@vue/language-core@3.1.5':
- resolution: {integrity: sha512-FMcqyzWN+sYBeqRMWPGT2QY0mUasZMVIuHvmb5NT3eeqPrbHBYtCP8JWEUCDCgM+Zr62uuWY/qoeBrPrzfa78w==}
+ '@vue/language-core@3.1.8':
+ resolution: {integrity: sha512-PfwAW7BLopqaJbneChNL6cUOTL3GL+0l8paYP5shhgY5toBNidWnMXWM+qDwL7MC9+zDtzCF2enT8r6VPu64iw==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
@@ -1944,8 +1946,8 @@ packages:
async@3.2.6:
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
- autoprefixer@10.4.22:
- resolution: {integrity: sha512-ARe0v/t9gO28Bznv6GgqARmVqcWOV3mfgUPn9becPHMiD3o9BwlRgaeccZnwTpZ7Zwqrm+c1sUSsMxIzQzc8Xg==}
+ autoprefixer@10.4.23:
+ resolution: {integrity: sha512-YYTXSFulfwytnjAPlw8QHncHJmlvFKtczb8InXaAx9Q0LbfDnfEYDE55omerIJKihhmU61Ft+cAOSzQVaBUmeA==}
engines: {node: ^10 || ^12 || >=14}
hasBin: true
peerDependencies:
@@ -1973,8 +1975,8 @@ packages:
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
- baseline-browser-mapping@2.9.2:
- resolution: {integrity: sha512-PxSsosKQjI38iXkmb3d0Y32efqyA0uW4s41u4IVBsLlWLhCiYNpH/AfNOVWRqCQBlD8TFJTz6OUWNd4DFJCnmw==}
+ baseline-browser-mapping@2.9.8:
+ resolution: {integrity: sha512-Y1fOuNDowLfgKOypdc9SPABfoWXuZHBOyCS4cD52IeZBhr4Md6CLLs6atcxVrzRmQ06E7hSlm5bHHApPKR/byA==}
hasBin: true
bidi-js@1.0.3:
@@ -2047,8 +2049,8 @@ packages:
caniuse-api@3.0.0:
resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==}
- caniuse-lite@1.0.30001759:
- resolution: {integrity: sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==}
+ caniuse-lite@1.0.30001760:
+ resolution: {integrity: sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==}
chai@6.2.1:
resolution: {integrity: sha512-p4Z49OGG5W/WBCPSS/dH3jQ73kD6tiMmUM+bckNK6Jr5JHMG3k9bg/BvKR8lKmtVBKmOiuVaV2ws8s9oSbwysg==}
@@ -2238,8 +2240,8 @@ packages:
resolution: {integrity: sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==}
engines: {node: ^10 || ^12.20.0 || ^14.13.0 || >=15.0.0, npm: '>=7.0.0'}
- cssstyle@5.3.3:
- resolution: {integrity: sha512-OytmFH+13/QXONJcC75QNdMtKpceNk3u8ThBjyyYjkEcy/ekBwR1mMAuNvi3gdBPW3N5TlCzQ0WZw8H0lN/bDw==}
+ cssstyle@5.3.4:
+ resolution: {integrity: sha512-KyOS/kJMEq5O9GdPnaf82noigg5X5DYn0kZPJTaAsCUaBizp6Xa1y9D4Qoqf/JazEXWuruErHgVXwjN5391ZJw==}
engines: {node: '>=20'}
csstype@3.2.3:
@@ -2339,8 +2341,8 @@ packages:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
- devalue@5.5.0:
- resolution: {integrity: sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==}
+ devalue@5.6.1:
+ resolution: {integrity: sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==}
diff@8.0.2:
resolution: {integrity: sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg==}
@@ -2380,8 +2382,8 @@ packages:
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
- electron-to-chromium@1.5.266:
- resolution: {integrity: sha512-kgWEglXvkEfMH7rxP5OSZZwnaDWT7J9EoZCujhnpLbfi0bbNtRkgdX2E3gt0Uer11c61qCYktB3hwkAS325sJg==}
+ electron-to-chromium@1.5.267:
+ resolution: {integrity: sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==}
emoji-regex@8.0.0:
resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==}
@@ -2491,8 +2493,8 @@ packages:
eslint-import-resolver-node:
optional: true
- eslint-plugin-jsdoc@61.4.1:
- resolution: {integrity: sha512-3c1QW/bV25sJ1MsIvsvW+EtLtN6yZMduw7LVQNVt72y2/5BbV5Pg5b//TE5T48LRUxoEQGaZJejCmcj3wCxBzw==}
+ eslint-plugin-jsdoc@61.5.0:
+ resolution: {integrity: sha512-PR81eOGq4S7diVnV9xzFSBE4CDENRQGP0Lckkek8AdHtbj+6Bm0cItwlFnxsLFriJHspiE3mpu8U20eODyToIg==}
engines: {node: '>=20.11.0'}
peerDependencies:
eslint: ^7.0.0 || ^8.0.0 || ^9.0.0
@@ -2546,8 +2548,8 @@ packages:
resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
- eslint@9.39.1:
- resolution: {integrity: sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==}
+ eslint@9.39.2:
+ resolution: {integrity: sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
hasBin: true
peerDependencies:
@@ -2601,8 +2603,8 @@ packages:
resolution: {integrity: sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==}
engines: {node: '>=16.17'}
- expect-type@1.2.2:
- resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==}
+ expect-type@1.3.0:
+ resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==}
engines: {node: '>=12.0.0'}
exsolve@1.0.8:
@@ -2763,9 +2765,6 @@ packages:
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
- graphemer@1.4.0:
- resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==}
-
gzip-size@7.0.0:
resolution: {integrity: sha512-O1Ld7Dr+nqPnmGpdhzLmMTQ4vAsD+rHwMm1NLUmoUFFymBOMKxCCrtDxqdBRYXdeEPEi3SyoR4TizJLQrnKBNA==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -3288,10 +3287,6 @@ packages:
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
engines: {node: '>=0.10.0'}
- normalize-range@0.1.2:
- resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==}
- engines: {node: '>=0.10.0'}
-
npm-run-path@5.3.0:
resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==}
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
@@ -3303,8 +3298,8 @@ packages:
nth-check@2.1.1:
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
- nuxt@4.2.1:
- resolution: {integrity: sha512-OE5ONizgwkKhjTGlUYB3ksE+2q2/I30QIYFl3N1yYz1r2rwhunGA3puUvqkzXwgLQ3AdsNcigPDmyQsqjbSdoQ==}
+ nuxt@4.2.2:
+ resolution: {integrity: sha512-n6oYFikgLEb70J4+K19jAzfx4exZcRSRX7yZn09P5qlf2Z59VNOBqNmaZO5ObzvyGUZ308SZfL629/Q2v2FVjw==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
@@ -3357,22 +3352,22 @@ packages:
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
engines: {node: '>= 0.8.0'}
- oxc-minify@0.96.0:
- resolution: {integrity: sha512-dXeeGrfPJJ4rMdw+NrqiCRtbzVX2ogq//R0Xns08zql2HjV3Zi2SBJ65saqfDaJzd2bcHqvGWH+M44EQCHPAcA==}
+ oxc-minify@0.102.0:
+ resolution: {integrity: sha512-FphAHDyTCNepQbiQTSyWFMbNc9zdUmj1WBsoLwvZhWm7rEe/IeIKYKRhy75lWOjwFsi5/i4Qucq43hgs3n2Exw==}
engines: {node: ^20.19.0 || >=22.12.0}
- oxc-parser@0.96.0:
- resolution: {integrity: sha512-ucs6niJ5mZlYP3oTl4AK2eD2m7WLoSaljswnSFVYWrXzme5PtM97S7Ve1Tjx+/TKjanmEZuSt1f1qYi6SZvntw==}
+ oxc-parser@0.102.0:
+ resolution: {integrity: sha512-xMiyHgr2FZsphQ12ZCsXRvSYzmKXCm1ejmyG4GDZIiKOmhyt5iKtWq0klOfFsEQ6jcgbwrUdwcCVYzr1F+h5og==}
engines: {node: ^20.19.0 || >=22.12.0}
- oxc-transform@0.96.0:
- resolution: {integrity: sha512-dQPNIF+gHpSkmC0+Vg9IktNyhcn28Y8R3eTLyzn52UNymkasLicl3sFAtz7oEVuFmCpgGjaUTKkwk+jW2cHpDQ==}
+ oxc-transform@0.102.0:
+ resolution: {integrity: sha512-MR5ohiBS6/kvxRpmUZ3LIDTTJBEC4xLAEZXfYr7vrA0eP7WHewQaNQPFDgT4Bee89TdmVQ5ZKrifGwxLjSyHHw==}
engines: {node: ^20.19.0 || >=22.12.0}
- oxc-walker@0.5.2:
- resolution: {integrity: sha512-XYoZqWwApSKUmSDEFeOKdy3Cdh95cOcSU8f7yskFWE4Rl3cfL5uwyY+EV7Brk9mdNLy+t5SseJajd6g7KncvlA==}
+ oxc-walker@0.6.0:
+ resolution: {integrity: sha512-BA3hlxq5+Sgzp7TCQF52XDXCK5mwoIZuIuxv/+JuuTzOs2RXkLqWZgZ69d8pJDDjnL7wiREZTWHBzFp/UWH88Q==}
peerDependencies:
- oxc-parser: '>=0.72.0'
+ oxc-parser: '>=0.98.0'
p-limit@3.1.0:
resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==}
@@ -3796,8 +3791,8 @@ packages:
rollup:
optional: true
- rollup@4.53.3:
- resolution: {integrity: sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==}
+ rollup@4.53.5:
+ resolution: {integrity: sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ==}
engines: {node: '>=18.0.0', npm: '>=8.0.0'}
hasBin: true
@@ -3843,8 +3838,8 @@ packages:
engines: {node: '>=10'}
hasBin: true
- send@1.2.0:
- resolution: {integrity: sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==}
+ send@1.2.1:
+ resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==}
engines: {node: '>= 18'}
serialize-javascript@6.0.2:
@@ -3857,8 +3852,8 @@ packages:
serve-placeholder@2.0.2:
resolution: {integrity: sha512-/TMG8SboeiQbZJWRlfTCqMs2DD3SZgWp0kDQePz9yUuCnDfDh/92gf7/PxGhzXTKBIPASIHxFcZndoNbp6QOLQ==}
- serve-static@2.2.0:
- resolution: {integrity: sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==}
+ serve-static@2.2.1:
+ resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==}
engines: {node: '>= 18'}
setprototypeof@1.2.0:
@@ -3936,8 +3931,8 @@ packages:
resolution: {integrity: sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==}
engines: {node: '>=0.10.0'}
- srvx@0.9.7:
- resolution: {integrity: sha512-N2a2nx8YTq13+A8qucg4lHZREfWOVnlMHAvrA9C2jbY9/QnVEAPzjdmpFHrY6/9BxSwIbvywCj7zahuGrVzCiQ==}
+ srvx@0.9.8:
+ resolution: {integrity: sha512-RZaxTKJEE/14HYn8COLuUOJAt0U55N9l1Xf6jj+T0GoA01EUH1Xz5JtSUOI+EHn+AEgPCVn7gk6jHJffrr06fQ==}
engines: {node: '>=20.16.0'}
hasBin: true
@@ -4119,8 +4114,8 @@ packages:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
- type-fest@5.3.0:
- resolution: {integrity: sha512-d9CwU93nN0IA1QL+GSNDdwLAu1Ew5ZjTwupvedwg3WdfoH6pIDvYQ2hV0Uc2nKBLPq7NB5apCx57MLS5qlmO5g==}
+ type-fest@5.3.1:
+ resolution: {integrity: sha512-VCn+LMHbd4t6sF3wfU/+HKT63C9OoyrSIf4b+vtWHpt2U7/4InZG467YDNMFMR70DdHjAdpPWmw2lzRdg0Xqqg==}
engines: {node: '>=20'}
type-level-regexp@0.1.17:
@@ -4156,8 +4151,8 @@ packages:
resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
engines: {node: '>=18'}
- unimport@5.5.0:
- resolution: {integrity: sha512-/JpWMG9s1nBSlXJAQ8EREFTFy3oy6USFd8T6AoBaw1q2GGcF4R9yp3ofg32UODZlYEO5VD0EWE1RpI9XDWyPYg==}
+ unimport@5.6.0:
+ resolution: {integrity: sha512-8rqAmtJV8o60x46kBAJKtHpJDJWkA2xcBqWKPI14MgUb05o1pnpnCnXSxedUXyeq7p8fR5g3pTo2BaswZ9lD9A==}
engines: {node: '>=18.12.0'}
unplugin-utils@0.2.5:
@@ -4168,8 +4163,8 @@ packages:
resolution: {integrity: sha512-5lWVjgi6vuHhJ526bI4nlCOmkCIF3nnfXkCMDeMJrtdvxTs6ZFCM8oNufGTsDbKv/tJ/xj8RpvXjRuPBZJuJog==}
engines: {node: '>=20.19.0'}
- unplugin-vue-router@0.16.2:
- resolution: {integrity: sha512-lE6ZjnHaXfS2vFI/PSEwdKcdOo5RwAbCKUnPBIN9YwLgSWas3x+qivzQvJa/uxhKzJldE6WK43aDKjGj9Rij9w==}
+ unplugin-vue-router@0.19.1:
+ resolution: {integrity: sha512-LJVRzfxS4j34K4sx4pggzhqpfAtXNZ6mLLRHvlSbDw11lWKLluuLXRbSWLXfiVj4RHeNHXu/+XxsGX65Ogu07Q==}
peerDependencies:
'@vue/compiler-sfc': ^3.5.17
vue-router: ^4.6.0
@@ -4257,8 +4252,8 @@ packages:
unwasm@0.3.11:
resolution: {integrity: sha512-Vhp5gb1tusSQw5of/g3Q697srYgMXvwMgXMjcG4ZNga02fDX9coxJ9fAb0Ci38hM2Hv/U1FXRPGgjP2BYqhNoQ==}
- update-browserslist-db@1.2.2:
- resolution: {integrity: sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==}
+ update-browserslist-db@1.2.3:
+ resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==}
hasBin: true
peerDependencies:
browserslist: '>= 4.21.0'
@@ -4287,18 +4282,18 @@ packages:
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
- vite-plugin-checker@0.11.0:
- resolution: {integrity: sha512-iUdO9Pl9UIBRPAragwi3as/BXXTtRu4G12L3CMrjx+WVTd9g/MsqNakreib9M/2YRVkhZYiTEwdH2j4Dm0w7lw==}
+ vite-plugin-checker@0.12.0:
+ resolution: {integrity: sha512-CmdZdDOGss7kdQwv73UyVgLPv0FVYe5czAgnmRX2oKljgEvSrODGuClaV3PDR2+3ou7N/OKGauDDBjy2MB07Rg==}
engines: {node: '>=16.11'}
peerDependencies:
'@biomejs/biome': '>=1.7'
- eslint: '>=7'
+ eslint: '>=9.39.1'
meow: ^13.2.0
optionator: ^0.9.4
oxlint: '>=1'
stylelint: '>=16'
typescript: '*'
- vite: '>=5.4.20'
+ vite: '>=5.4.21'
vls: '*'
vti: '*'
vue-tsc: ~2.2.10 || ^3.0.0
@@ -4334,14 +4329,14 @@ packages:
'@nuxt/kit':
optional: true
- vite-plugin-vue-tracer@1.1.3:
- resolution: {integrity: sha512-fM7hfHELZvbPnSn8EKZwHfzxm5EfYFQIclz8rwcNXfodNbRkwNvh0AGMtaBfMxQ9HC5KVa3KitwHnmE4ezDemw==}
+ vite-plugin-vue-tracer@1.2.0:
+ resolution: {integrity: sha512-a9Z/TLpxwmoE9kIcv28wqQmiszM7ec4zgndXWEsVD/2lEZLRGzcg7ONXmplzGF/UP5W59QNtS809OdywwpUWQQ==}
peerDependencies:
vite: ^6.0.0 || ^7.0.0
vue: ^3.5.0
- vite@7.2.6:
- resolution: {integrity: sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==}
+ vite@7.3.0:
+ resolution: {integrity: sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==}
engines: {node: ^20.19.0 || >=22.12.0}
hasBin: true
peerDependencies:
@@ -4380,18 +4375,18 @@ packages:
yaml:
optional: true
- vitest@4.0.15:
- resolution: {integrity: sha512-n1RxDp8UJm6N0IbJLQo+yzLZ2sQCDyl1o0LeugbPWf8+8Fttp29GghsQBjYJVmWq3gBFfe9Hs1spR44vovn2wA==}
+ vitest@4.0.16:
+ resolution: {integrity: sha512-E4t7DJ9pESL6E3I8nFjPa4xGUd3PmiWDLsDztS2qXSJWfHtbQnwAWylaBvSNY48I3vr8PTqIZlyK8TE3V3CA4Q==}
engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0}
hasBin: true
peerDependencies:
'@edge-runtime/vm': '*'
'@opentelemetry/api': ^1.9.0
'@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0
- '@vitest/browser-playwright': 4.0.15
- '@vitest/browser-preview': 4.0.15
- '@vitest/browser-webdriverio': 4.0.15
- '@vitest/ui': 4.0.15
+ '@vitest/browser-playwright': 4.0.16
+ '@vitest/browser-preview': 4.0.16
+ '@vitest/browser-webdriverio': 4.0.16
+ '@vitest/ui': 4.0.16
happy-dom: '*'
jsdom: '*'
peerDependenciesMeta:
@@ -4434,8 +4429,8 @@ packages:
peerDependencies:
vue: ^3.0.0
- vue-router@4.6.3:
- resolution: {integrity: sha512-ARBedLm9YlbvQomnmq91Os7ck6efydTSpRP3nuOKCvgJOHNrhRoJDSKtee8kcL1Vf7nz6U+PMBL+hTvR3bTVQg==}
+ vue-router@4.6.4:
+ resolution: {integrity: sha512-Hz9q5sa33Yhduglwz6g9skT8OBPii+4bFn88w6J+J4MfEo4KRRpmiNG/hHHkdbRFlLBOqxN8y8gf2Fb0MTUgVg==}
peerDependencies:
vue: ^3.5.0
@@ -4444,8 +4439,8 @@ packages:
peerDependencies:
vue: ^3.0.2
- vue-tsc@3.1.5:
- resolution: {integrity: sha512-L/G9IUjOWhBU0yun89rv8fKqmKC+T0HfhrFjlIml71WpfBv9eb4E9Bev8FMbyueBIU9vxQqbd+oOsVcDa5amGw==}
+ vue-tsc@3.1.8:
+ resolution: {integrity: sha512-deKgwx6exIHeZwF601P1ktZKNF0bepaSN4jBU3AsbldPx9gylUc1JDxYppl82yxgkAgaz0Y0LCLOi+cXe9HMYA==}
hasBin: true
peerDependencies:
typescript: '>=5.0.0'
@@ -4611,7 +4606,7 @@ snapshots:
'@types/json-schema': 7.0.15
js-yaml: 4.1.1
- '@asamuzakjp/css-color@4.1.0':
+ '@asamuzakjp/css-color@4.1.1':
dependencies:
'@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
'@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)
@@ -4620,7 +4615,7 @@ snapshots:
lru-cache: 11.2.4
optional: true
- '@asamuzakjp/dom-selector@6.7.5':
+ '@asamuzakjp/dom-selector@6.7.6':
dependencies:
'@asamuzakjp/nwsapi': 2.3.9
bidi-js: 1.0.3
@@ -4852,7 +4847,9 @@ snapshots:
'@csstools/css-tokenizer': 3.0.4
optional: true
- '@csstools/css-syntax-patches-for-csstree@1.0.20':
+ '@csstools/css-syntax-patches-for-csstree@1.0.14(postcss@8.5.6)':
+ dependencies:
+ postcss: 8.5.6
optional: true
'@csstools/css-tokenizer@3.0.4':
@@ -4861,7 +4858,7 @@ snapshots:
'@dxup/nuxt@0.2.2(magicast@0.5.1)':
dependencies:
'@dxup/unimport': 0.1.2
- '@nuxt/kit': 4.2.1(magicast@0.5.1)
+ '@nuxt/kit': 4.2.2(magicast@0.5.1)
chokidar: 4.0.3
pathe: 2.0.3
tinyglobby: 0.2.15
@@ -4889,7 +4886,7 @@ snapshots:
'@es-joy/jsdoccomment@0.76.0':
dependencies:
'@types/estree': 1.0.8
- '@typescript-eslint/types': 8.48.1
+ '@typescript-eslint/types': 8.50.0
comment-parser: 1.4.1
esquery: 1.6.0
jsdoc-type-pratt-parser: 6.10.0
@@ -5052,18 +5049,18 @@ snapshots:
'@esbuild/win32-x64@0.27.1':
optional: true
- '@eslint-community/eslint-utils@4.9.0(eslint@9.39.1(jiti@2.6.1))':
+ '@eslint-community/eslint-utils@4.9.0(eslint@9.39.2(jiti@2.6.1))':
dependencies:
- eslint: 9.39.1(jiti@2.6.1)
+ eslint: 9.39.2(jiti@2.6.1)
eslint-visitor-keys: 3.4.3
'@eslint-community/regexpp@4.12.2': {}
- '@eslint/compat@1.4.1(eslint@9.39.1(jiti@2.6.1))':
+ '@eslint/compat@1.4.1(eslint@9.39.2(jiti@2.6.1))':
dependencies:
'@eslint/core': 0.17.0
optionalDependencies:
- eslint: 9.39.1(jiti@2.6.1)
+ eslint: 9.39.2(jiti@2.6.1)
'@eslint/config-array@0.21.1':
dependencies:
@@ -5077,14 +5074,14 @@ snapshots:
dependencies:
'@eslint/core': 0.17.0
- '@eslint/config-inspector@1.4.2(eslint@9.39.1(jiti@2.6.1))':
+ '@eslint/config-inspector@1.4.2(eslint@9.39.2(jiti@2.6.1))':
dependencies:
ansis: 4.2.0
bundle-require: 5.1.0(esbuild@0.27.1)
cac: 6.7.14
chokidar: 4.0.3
esbuild: 0.27.1
- eslint: 9.39.1(jiti@2.6.1)
+ eslint: 9.39.2(jiti@2.6.1)
h3: 1.15.4
tinyglobby: 0.2.15
ws: 8.18.3
@@ -5110,7 +5107,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@eslint/js@9.39.1': {}
+ '@eslint/js@9.39.2': {}
'@eslint/object-schema@2.1.7': {}
@@ -5232,7 +5229,7 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.19.1
- '@nuxt/cli@3.31.1(cac@6.7.14)(magicast@0.5.1)':
+ '@nuxt/cli@3.31.2(cac@6.7.14)(magicast@0.5.1)':
dependencies:
'@bomb.sh/tab': 0.0.9(cac@6.7.14)(citty@0.1.6)
'@clack/prompts': 1.0.0-alpha.7
@@ -5256,7 +5253,7 @@ snapshots:
pkg-types: 2.3.0
scule: 1.3.0
semver: 7.7.3
- srvx: 0.9.7
+ srvx: 0.9.8
std-env: 3.10.0
tinyexec: 1.0.2
ufo: 1.6.1
@@ -5269,11 +5266,11 @@ snapshots:
'@nuxt/devalue@2.0.2': {}
- '@nuxt/devtools-kit@3.1.1(magicast@0.5.1)(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))':
+ '@nuxt/devtools-kit@3.1.1(magicast@0.5.1)(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))':
dependencies:
- '@nuxt/kit': 4.2.1(magicast@0.5.1)
+ '@nuxt/kit': 4.2.2(magicast@0.5.1)
execa: 8.0.1
- vite: 7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
+ vite: 7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
transitivePeerDependencies:
- magicast
@@ -5288,12 +5285,12 @@ snapshots:
prompts: 2.4.2
semver: 7.7.3
- '@nuxt/devtools@3.1.1(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))':
+ '@nuxt/devtools@3.1.1(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))':
dependencies:
- '@nuxt/devtools-kit': 3.1.1(magicast@0.5.1)(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))
+ '@nuxt/devtools-kit': 3.1.1(magicast@0.5.1)(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))
'@nuxt/devtools-wizard': 3.1.1
- '@nuxt/kit': 4.2.1(magicast@0.5.1)
- '@vue/devtools-core': 8.0.5(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))
+ '@nuxt/kit': 4.2.2(magicast@0.5.1)
+ '@vue/devtools-core': 8.0.5(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))
'@vue/devtools-kit': 8.0.5
birpc: 2.9.0
consola: 3.4.2
@@ -5318,9 +5315,9 @@ snapshots:
sirv: 3.0.2
structured-clone-es: 1.0.0
tinyglobby: 0.2.15
- vite: 7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
- vite-plugin-inspect: 11.3.3(@nuxt/kit@4.2.1(magicast@0.5.1))(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))
- vite-plugin-vue-tracer: 1.1.3(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))
+ vite: 7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
+ vite-plugin-inspect: 11.3.3(@nuxt/kit@4.2.2(magicast@0.5.1))(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))
+ vite-plugin-vue-tracer: 1.2.0(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))
which: 5.0.0
ws: 8.18.3
transitivePeerDependencies:
@@ -5329,30 +5326,30 @@ snapshots:
- utf-8-validate
- vue
- '@nuxt/eslint-config@1.11.0(@typescript-eslint/utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.25)(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
+ '@nuxt/eslint-config@1.12.1(@typescript-eslint/utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.25)(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@antfu/install-pkg': 1.1.0
'@clack/prompts': 0.11.0
- '@eslint/js': 9.39.1
- '@nuxt/eslint-plugin': 1.11.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
- '@stylistic/eslint-plugin': 5.6.1(eslint@9.39.1(jiti@2.6.1))
- '@typescript-eslint/eslint-plugin': 8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
- '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
- eslint: 9.39.1(jiti@2.6.1)
- eslint-config-flat-gitignore: 2.1.0(eslint@9.39.1(jiti@2.6.1))
+ '@eslint/js': 9.39.2
+ '@nuxt/eslint-plugin': 1.12.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@stylistic/eslint-plugin': 5.6.1(eslint@9.39.2(jiti@2.6.1))
+ '@typescript-eslint/eslint-plugin': 8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/parser': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ eslint: 9.39.2(jiti@2.6.1)
+ eslint-config-flat-gitignore: 2.1.0(eslint@9.39.2(jiti@2.6.1))
eslint-flat-config-utils: 2.1.4
- eslint-merge-processors: 2.0.0(eslint@9.39.1(jiti@2.6.1))
- eslint-plugin-import-lite: 0.3.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
- eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))
- eslint-plugin-jsdoc: 61.4.1(eslint@9.39.1(jiti@2.6.1))
- eslint-plugin-regexp: 2.10.0(eslint@9.39.1(jiti@2.6.1))
- eslint-plugin-unicorn: 62.0.0(eslint@9.39.1(jiti@2.6.1))
- eslint-plugin-vue: 10.6.2(@stylistic/eslint-plugin@5.6.1(eslint@9.39.1(jiti@2.6.1)))(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.39.1(jiti@2.6.1)))
- eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.25)(eslint@9.39.1(jiti@2.6.1))
+ eslint-merge-processors: 2.0.0(eslint@9.39.2(jiti@2.6.1))
+ eslint-plugin-import-lite: 0.3.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ eslint-plugin-import-x: 4.16.1(@typescript-eslint/utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))
+ eslint-plugin-jsdoc: 61.5.0(eslint@9.39.2(jiti@2.6.1))
+ eslint-plugin-regexp: 2.10.0(eslint@9.39.2(jiti@2.6.1))
+ eslint-plugin-unicorn: 62.0.0(eslint@9.39.2(jiti@2.6.1))
+ eslint-plugin-vue: 10.6.2(@stylistic/eslint-plugin@5.6.1(eslint@9.39.2(jiti@2.6.1)))(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.39.2(jiti@2.6.1)))
+ eslint-processor-vue-blocks: 2.0.0(@vue/compiler-sfc@3.5.25)(eslint@9.39.2(jiti@2.6.1))
globals: 16.5.0
local-pkg: 1.1.2
pathe: 2.0.3
- vue-eslint-parser: 10.2.0(eslint@9.39.1(jiti@2.6.1))
+ vue-eslint-parser: 10.2.0(eslint@9.39.2(jiti@2.6.1))
transitivePeerDependencies:
- '@typescript-eslint/utils'
- '@vue/compiler-sfc'
@@ -5360,31 +5357,31 @@ snapshots:
- supports-color
- typescript
- '@nuxt/eslint-plugin@1.11.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
+ '@nuxt/eslint-plugin@1.12.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
- '@typescript-eslint/types': 8.48.1
- '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
- eslint: 9.39.1(jiti@2.6.1)
+ '@typescript-eslint/types': 8.50.0
+ '@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ eslint: 9.39.2(jiti@2.6.1)
transitivePeerDependencies:
- supports-color
- typescript
- '@nuxt/eslint@1.11.0(@typescript-eslint/utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.25)(eslint@9.39.1(jiti@2.6.1))(magicast@0.5.1)(typescript@5.9.3)(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))':
+ '@nuxt/eslint@1.12.1(@typescript-eslint/utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.25)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.1)(typescript@5.9.3)(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))':
dependencies:
- '@eslint/config-inspector': 1.4.2(eslint@9.39.1(jiti@2.6.1))
- '@nuxt/devtools-kit': 3.1.1(magicast@0.5.1)(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))
- '@nuxt/eslint-config': 1.11.0(@typescript-eslint/utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.25)(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
- '@nuxt/eslint-plugin': 1.11.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
- '@nuxt/kit': 4.2.1(magicast@0.5.1)
+ '@eslint/config-inspector': 1.4.2(eslint@9.39.2(jiti@2.6.1))
+ '@nuxt/devtools-kit': 3.1.1(magicast@0.5.1)(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))
+ '@nuxt/eslint-config': 1.12.1(@typescript-eslint/utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(@vue/compiler-sfc@3.5.25)(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@nuxt/eslint-plugin': 1.12.1(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@nuxt/kit': 4.2.2(magicast@0.5.1)
chokidar: 5.0.0
- eslint: 9.39.1(jiti@2.6.1)
+ eslint: 9.39.2(jiti@2.6.1)
eslint-flat-config-utils: 2.1.4
- eslint-typegen: 2.3.0(eslint@9.39.1(jiti@2.6.1))
+ eslint-typegen: 2.3.0(eslint@9.39.2(jiti@2.6.1))
find-up: 8.0.0
get-port-please: 3.2.0
mlly: 1.8.0
pathe: 2.0.3
- unimport: 5.5.0
+ unimport: 5.6.0
transitivePeerDependencies:
- '@typescript-eslint/utils'
- '@vue/compiler-sfc'
@@ -5397,7 +5394,7 @@ snapshots:
- utf-8-validate
- vite
- '@nuxt/kit@3.20.1(magicast@0.5.1)':
+ '@nuxt/kit@3.20.2(magicast@0.5.1)':
dependencies:
c12: 3.3.2(magicast@0.5.1)
consola: 3.4.2
@@ -5423,7 +5420,7 @@ snapshots:
transitivePeerDependencies:
- magicast
- '@nuxt/kit@4.2.1(magicast@0.5.1)':
+ '@nuxt/kit@4.2.2(magicast@0.5.1)':
dependencies:
c12: 3.3.2(magicast@0.5.1)
consola: 3.4.2
@@ -5448,16 +5445,16 @@ snapshots:
transitivePeerDependencies:
- magicast
- '@nuxt/nitro-server@4.2.1(db0@0.3.4)(ioredis@5.8.2)(magicast@0.5.1)(nuxt@4.2.1(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.25)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.3)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.5(typescript@5.9.3))(yaml@2.8.2))(typescript@5.9.3)':
+ '@nuxt/nitro-server@4.2.2(db0@0.3.4)(ioredis@5.8.2)(magicast@0.5.1)(nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.25)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.5)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.8(typescript@5.9.3))(yaml@2.8.2))(typescript@5.9.3)':
dependencies:
'@nuxt/devalue': 2.0.2
- '@nuxt/kit': 4.2.1(magicast@0.5.1)
+ '@nuxt/kit': 4.2.2(magicast@0.5.1)
'@unhead/vue': 2.0.19(vue@3.5.25(typescript@5.9.3))
'@vue/shared': 3.5.25
consola: 3.4.2
defu: 6.1.4
destr: 2.0.5
- devalue: 5.5.0
+ devalue: 5.6.1
errx: 0.1.0
escape-string-regexp: 5.0.0
exsolve: 1.0.8
@@ -5466,7 +5463,7 @@ snapshots:
klona: 2.0.6
mocked-exports: 0.1.1
nitropack: 2.12.9
- nuxt: 4.2.1(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.25)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.3)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.5(typescript@5.9.3))(yaml@2.8.2)
+ nuxt: 4.2.2(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.25)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.5)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.8(typescript@5.9.3))(yaml@2.8.2)
pathe: 2.0.3
pkg-types: 2.3.0
radix3: 1.1.2
@@ -5512,7 +5509,7 @@ snapshots:
- uploadthing
- xml2js
- '@nuxt/schema@4.2.1':
+ '@nuxt/schema@4.2.2':
dependencies:
'@vue/shared': 3.5.25
defu: 6.1.4
@@ -5522,7 +5519,7 @@ snapshots:
'@nuxt/telemetry@2.6.6(magicast@0.5.1)':
dependencies:
- '@nuxt/kit': 3.20.1(magicast@0.5.1)
+ '@nuxt/kit': 3.20.2(magicast@0.5.1)
citty: 0.1.6
consola: 3.4.2
destr: 2.0.5
@@ -5537,17 +5534,17 @@ snapshots:
transitivePeerDependencies:
- magicast
- '@nuxt/vite-builder@4.2.1(@types/node@22.18.1)(eslint@9.39.1(jiti@2.6.1))(magicast@0.5.1)(nuxt@4.2.1(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.25)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.3)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.5(typescript@5.9.3))(yaml@2.8.2))(optionator@0.9.4)(rollup@4.53.3)(terser@5.44.1)(typescript@5.9.3)(vue-tsc@3.1.5(typescript@5.9.3))(vue@3.5.25(typescript@5.9.3))(yaml@2.8.2)':
+ '@nuxt/vite-builder@4.2.2(@types/node@22.18.1)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.1)(nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.25)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.5)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.8(typescript@5.9.3))(yaml@2.8.2))(optionator@0.9.4)(rollup@4.53.5)(terser@5.44.1)(typescript@5.9.3)(vue-tsc@3.1.8(typescript@5.9.3))(vue@3.5.25(typescript@5.9.3))(yaml@2.8.2)':
dependencies:
- '@nuxt/kit': 4.2.1(magicast@0.5.1)
- '@rollup/plugin-replace': 6.0.3(rollup@4.53.3)
- '@vitejs/plugin-vue': 6.0.2(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))
- '@vitejs/plugin-vue-jsx': 5.1.2(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))
- autoprefixer: 10.4.22(postcss@8.5.6)
+ '@nuxt/kit': 4.2.2(magicast@0.5.1)
+ '@rollup/plugin-replace': 6.0.3(rollup@4.53.5)
+ '@vitejs/plugin-vue': 6.0.3(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))
+ '@vitejs/plugin-vue-jsx': 5.1.2(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))
+ autoprefixer: 10.4.23(postcss@8.5.6)
consola: 3.4.2
cssnano: 7.1.2(postcss@8.5.6)
defu: 6.1.4
- esbuild: 0.25.12
+ esbuild: 0.27.1
escape-string-regexp: 5.0.0
exsolve: 1.0.8
get-port-please: 3.2.0
@@ -5557,18 +5554,18 @@ snapshots:
magic-string: 0.30.21
mlly: 1.8.0
mocked-exports: 0.1.1
- nuxt: 4.2.1(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.25)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.3)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.5(typescript@5.9.3))(yaml@2.8.2)
+ nuxt: 4.2.2(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.25)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.5)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.8(typescript@5.9.3))(yaml@2.8.2)
pathe: 2.0.3
pkg-types: 2.3.0
postcss: 8.5.6
- rollup-plugin-visualizer: 6.0.5(rollup@4.53.3)
+ rollup-plugin-visualizer: 6.0.5(rollup@4.53.5)
seroval: 1.4.0
std-env: 3.10.0
ufo: 1.6.1
unenv: 2.0.0-rc.24
- vite: 7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
+ vite: 7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
vite-node: 5.2.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
- vite-plugin-checker: 0.11.0(eslint@9.39.1(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.5(typescript@5.9.3))
+ vite-plugin-checker: 0.12.0(eslint@9.39.2(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.8(typescript@5.9.3))
vue: 3.5.25(typescript@5.9.3)
vue-bundle-renderer: 2.2.0
transitivePeerDependencies:
@@ -5596,147 +5593,147 @@ snapshots:
- vue-tsc
- yaml
- '@oxc-minify/binding-android-arm64@0.96.0':
+ '@oxc-minify/binding-android-arm64@0.102.0':
optional: true
- '@oxc-minify/binding-darwin-arm64@0.96.0':
+ '@oxc-minify/binding-darwin-arm64@0.102.0':
optional: true
- '@oxc-minify/binding-darwin-x64@0.96.0':
+ '@oxc-minify/binding-darwin-x64@0.102.0':
optional: true
- '@oxc-minify/binding-freebsd-x64@0.96.0':
+ '@oxc-minify/binding-freebsd-x64@0.102.0':
optional: true
- '@oxc-minify/binding-linux-arm-gnueabihf@0.96.0':
+ '@oxc-minify/binding-linux-arm-gnueabihf@0.102.0':
optional: true
- '@oxc-minify/binding-linux-arm-musleabihf@0.96.0':
+ '@oxc-minify/binding-linux-arm64-gnu@0.102.0':
optional: true
- '@oxc-minify/binding-linux-arm64-gnu@0.96.0':
+ '@oxc-minify/binding-linux-arm64-musl@0.102.0':
optional: true
- '@oxc-minify/binding-linux-arm64-musl@0.96.0':
+ '@oxc-minify/binding-linux-riscv64-gnu@0.102.0':
optional: true
- '@oxc-minify/binding-linux-riscv64-gnu@0.96.0':
+ '@oxc-minify/binding-linux-s390x-gnu@0.102.0':
optional: true
- '@oxc-minify/binding-linux-s390x-gnu@0.96.0':
+ '@oxc-minify/binding-linux-x64-gnu@0.102.0':
optional: true
- '@oxc-minify/binding-linux-x64-gnu@0.96.0':
+ '@oxc-minify/binding-linux-x64-musl@0.102.0':
optional: true
- '@oxc-minify/binding-linux-x64-musl@0.96.0':
+ '@oxc-minify/binding-openharmony-arm64@0.102.0':
optional: true
- '@oxc-minify/binding-wasm32-wasi@0.96.0':
+ '@oxc-minify/binding-wasm32-wasi@0.102.0':
dependencies:
'@napi-rs/wasm-runtime': 1.1.0
optional: true
- '@oxc-minify/binding-win32-arm64-msvc@0.96.0':
+ '@oxc-minify/binding-win32-arm64-msvc@0.102.0':
optional: true
- '@oxc-minify/binding-win32-x64-msvc@0.96.0':
+ '@oxc-minify/binding-win32-x64-msvc@0.102.0':
optional: true
- '@oxc-parser/binding-android-arm64@0.96.0':
+ '@oxc-parser/binding-android-arm64@0.102.0':
optional: true
- '@oxc-parser/binding-darwin-arm64@0.96.0':
+ '@oxc-parser/binding-darwin-arm64@0.102.0':
optional: true
- '@oxc-parser/binding-darwin-x64@0.96.0':
+ '@oxc-parser/binding-darwin-x64@0.102.0':
optional: true
- '@oxc-parser/binding-freebsd-x64@0.96.0':
+ '@oxc-parser/binding-freebsd-x64@0.102.0':
optional: true
- '@oxc-parser/binding-linux-arm-gnueabihf@0.96.0':
+ '@oxc-parser/binding-linux-arm-gnueabihf@0.102.0':
optional: true
- '@oxc-parser/binding-linux-arm-musleabihf@0.96.0':
+ '@oxc-parser/binding-linux-arm64-gnu@0.102.0':
optional: true
- '@oxc-parser/binding-linux-arm64-gnu@0.96.0':
+ '@oxc-parser/binding-linux-arm64-musl@0.102.0':
optional: true
- '@oxc-parser/binding-linux-arm64-musl@0.96.0':
+ '@oxc-parser/binding-linux-riscv64-gnu@0.102.0':
optional: true
- '@oxc-parser/binding-linux-riscv64-gnu@0.96.0':
+ '@oxc-parser/binding-linux-s390x-gnu@0.102.0':
optional: true
- '@oxc-parser/binding-linux-s390x-gnu@0.96.0':
+ '@oxc-parser/binding-linux-x64-gnu@0.102.0':
optional: true
- '@oxc-parser/binding-linux-x64-gnu@0.96.0':
+ '@oxc-parser/binding-linux-x64-musl@0.102.0':
optional: true
- '@oxc-parser/binding-linux-x64-musl@0.96.0':
+ '@oxc-parser/binding-openharmony-arm64@0.102.0':
optional: true
- '@oxc-parser/binding-wasm32-wasi@0.96.0':
+ '@oxc-parser/binding-wasm32-wasi@0.102.0':
dependencies:
'@napi-rs/wasm-runtime': 1.1.0
optional: true
- '@oxc-parser/binding-win32-arm64-msvc@0.96.0':
+ '@oxc-parser/binding-win32-arm64-msvc@0.102.0':
optional: true
- '@oxc-parser/binding-win32-x64-msvc@0.96.0':
+ '@oxc-parser/binding-win32-x64-msvc@0.102.0':
optional: true
- '@oxc-project/types@0.96.0': {}
+ '@oxc-project/types@0.102.0': {}
- '@oxc-transform/binding-android-arm64@0.96.0':
+ '@oxc-transform/binding-android-arm64@0.102.0':
optional: true
- '@oxc-transform/binding-darwin-arm64@0.96.0':
+ '@oxc-transform/binding-darwin-arm64@0.102.0':
optional: true
- '@oxc-transform/binding-darwin-x64@0.96.0':
+ '@oxc-transform/binding-darwin-x64@0.102.0':
optional: true
- '@oxc-transform/binding-freebsd-x64@0.96.0':
+ '@oxc-transform/binding-freebsd-x64@0.102.0':
optional: true
- '@oxc-transform/binding-linux-arm-gnueabihf@0.96.0':
+ '@oxc-transform/binding-linux-arm-gnueabihf@0.102.0':
optional: true
- '@oxc-transform/binding-linux-arm-musleabihf@0.96.0':
+ '@oxc-transform/binding-linux-arm64-gnu@0.102.0':
optional: true
- '@oxc-transform/binding-linux-arm64-gnu@0.96.0':
+ '@oxc-transform/binding-linux-arm64-musl@0.102.0':
optional: true
- '@oxc-transform/binding-linux-arm64-musl@0.96.0':
+ '@oxc-transform/binding-linux-riscv64-gnu@0.102.0':
optional: true
- '@oxc-transform/binding-linux-riscv64-gnu@0.96.0':
+ '@oxc-transform/binding-linux-s390x-gnu@0.102.0':
optional: true
- '@oxc-transform/binding-linux-s390x-gnu@0.96.0':
+ '@oxc-transform/binding-linux-x64-gnu@0.102.0':
optional: true
- '@oxc-transform/binding-linux-x64-gnu@0.96.0':
+ '@oxc-transform/binding-linux-x64-musl@0.102.0':
optional: true
- '@oxc-transform/binding-linux-x64-musl@0.96.0':
+ '@oxc-transform/binding-openharmony-arm64@0.102.0':
optional: true
- '@oxc-transform/binding-wasm32-wasi@0.96.0':
+ '@oxc-transform/binding-wasm32-wasi@0.102.0':
dependencies:
'@napi-rs/wasm-runtime': 1.1.0
optional: true
- '@oxc-transform/binding-win32-arm64-msvc@0.96.0':
+ '@oxc-transform/binding-win32-arm64-msvc@0.102.0':
optional: true
- '@oxc-transform/binding-win32-x64-msvc@0.96.0':
+ '@oxc-transform/binding-win32-x64-msvc@0.102.0':
optional: true
'@parcel/watcher-android-arm64@2.5.1':
@@ -5806,7 +5803,7 @@ snapshots:
'@pinia/nuxt@0.11.3(magicast@0.5.1)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)))':
dependencies:
- '@nuxt/kit': 4.2.1(magicast@0.5.1)
+ '@nuxt/kit': 4.2.2(magicast@0.5.1)
pinia: 3.0.4(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
transitivePeerDependencies:
- magicast
@@ -5816,29 +5813,29 @@ snapshots:
'@polka/url@1.0.0-next.29': {}
- '@poppinss/colors@4.1.5':
+ '@poppinss/colors@4.1.6':
dependencies:
kleur: 4.1.5
'@poppinss/dumper@0.6.5':
dependencies:
- '@poppinss/colors': 4.1.5
+ '@poppinss/colors': 4.1.6
'@sindresorhus/is': 7.1.1
supports-color: 10.2.2
- '@poppinss/exception@1.2.2': {}
-
- '@rolldown/pluginutils@1.0.0-beta.50': {}
+ '@poppinss/exception@1.2.3': {}
'@rolldown/pluginutils@1.0.0-beta.53': {}
- '@rollup/plugin-alias@5.1.1(rollup@4.53.3)':
- optionalDependencies:
- rollup: 4.53.3
+ '@rolldown/pluginutils@1.0.0-beta.9-commit.d91dfb5': {}
- '@rollup/plugin-commonjs@28.0.9(rollup@4.53.3)':
+ '@rollup/plugin-alias@5.1.1(rollup@4.53.5)':
+ optionalDependencies:
+ rollup: 4.53.5
+
+ '@rollup/plugin-commonjs@28.0.9(rollup@4.53.5)':
dependencies:
- '@rollup/pluginutils': 5.3.0(rollup@4.53.3)
+ '@rollup/pluginutils': 5.3.0(rollup@4.53.5)
commondir: 1.0.1
estree-walker: 2.0.2
fdir: 6.5.0(picomatch@4.0.3)
@@ -5846,119 +5843,119 @@ snapshots:
magic-string: 0.30.21
picomatch: 4.0.3
optionalDependencies:
- rollup: 4.53.3
+ rollup: 4.53.5
- '@rollup/plugin-inject@5.0.5(rollup@4.53.3)':
+ '@rollup/plugin-inject@5.0.5(rollup@4.53.5)':
dependencies:
- '@rollup/pluginutils': 5.3.0(rollup@4.53.3)
+ '@rollup/pluginutils': 5.3.0(rollup@4.53.5)
estree-walker: 2.0.2
magic-string: 0.30.21
optionalDependencies:
- rollup: 4.53.3
+ rollup: 4.53.5
- '@rollup/plugin-json@6.1.0(rollup@4.53.3)':
+ '@rollup/plugin-json@6.1.0(rollup@4.53.5)':
dependencies:
- '@rollup/pluginutils': 5.3.0(rollup@4.53.3)
+ '@rollup/pluginutils': 5.3.0(rollup@4.53.5)
optionalDependencies:
- rollup: 4.53.3
+ rollup: 4.53.5
- '@rollup/plugin-node-resolve@16.0.3(rollup@4.53.3)':
+ '@rollup/plugin-node-resolve@16.0.3(rollup@4.53.5)':
dependencies:
- '@rollup/pluginutils': 5.3.0(rollup@4.53.3)
+ '@rollup/pluginutils': 5.3.0(rollup@4.53.5)
'@types/resolve': 1.20.2
deepmerge: 4.3.1
is-module: 1.0.0
resolve: 1.22.11
optionalDependencies:
- rollup: 4.53.3
+ rollup: 4.53.5
- '@rollup/plugin-replace@6.0.3(rollup@4.53.3)':
+ '@rollup/plugin-replace@6.0.3(rollup@4.53.5)':
dependencies:
- '@rollup/pluginutils': 5.3.0(rollup@4.53.3)
+ '@rollup/pluginutils': 5.3.0(rollup@4.53.5)
magic-string: 0.30.21
optionalDependencies:
- rollup: 4.53.3
+ rollup: 4.53.5
- '@rollup/plugin-terser@0.4.4(rollup@4.53.3)':
+ '@rollup/plugin-terser@0.4.4(rollup@4.53.5)':
dependencies:
serialize-javascript: 6.0.2
smob: 1.5.0
terser: 5.44.1
optionalDependencies:
- rollup: 4.53.3
+ rollup: 4.53.5
- '@rollup/pluginutils@5.3.0(rollup@4.53.3)':
+ '@rollup/pluginutils@5.3.0(rollup@4.53.5)':
dependencies:
'@types/estree': 1.0.8
estree-walker: 2.0.2
picomatch: 4.0.3
optionalDependencies:
- rollup: 4.53.3
+ rollup: 4.53.5
- '@rollup/rollup-android-arm-eabi@4.53.3':
+ '@rollup/rollup-android-arm-eabi@4.53.5':
optional: true
- '@rollup/rollup-android-arm64@4.53.3':
+ '@rollup/rollup-android-arm64@4.53.5':
optional: true
- '@rollup/rollup-darwin-arm64@4.53.3':
+ '@rollup/rollup-darwin-arm64@4.53.5':
optional: true
- '@rollup/rollup-darwin-x64@4.53.3':
+ '@rollup/rollup-darwin-x64@4.53.5':
optional: true
- '@rollup/rollup-freebsd-arm64@4.53.3':
+ '@rollup/rollup-freebsd-arm64@4.53.5':
optional: true
- '@rollup/rollup-freebsd-x64@4.53.3':
+ '@rollup/rollup-freebsd-x64@4.53.5':
optional: true
- '@rollup/rollup-linux-arm-gnueabihf@4.53.3':
+ '@rollup/rollup-linux-arm-gnueabihf@4.53.5':
optional: true
- '@rollup/rollup-linux-arm-musleabihf@4.53.3':
+ '@rollup/rollup-linux-arm-musleabihf@4.53.5':
optional: true
- '@rollup/rollup-linux-arm64-gnu@4.53.3':
+ '@rollup/rollup-linux-arm64-gnu@4.53.5':
optional: true
- '@rollup/rollup-linux-arm64-musl@4.53.3':
+ '@rollup/rollup-linux-arm64-musl@4.53.5':
optional: true
- '@rollup/rollup-linux-loong64-gnu@4.53.3':
+ '@rollup/rollup-linux-loong64-gnu@4.53.5':
optional: true
- '@rollup/rollup-linux-ppc64-gnu@4.53.3':
+ '@rollup/rollup-linux-ppc64-gnu@4.53.5':
optional: true
- '@rollup/rollup-linux-riscv64-gnu@4.53.3':
+ '@rollup/rollup-linux-riscv64-gnu@4.53.5':
optional: true
- '@rollup/rollup-linux-riscv64-musl@4.53.3':
+ '@rollup/rollup-linux-riscv64-musl@4.53.5':
optional: true
- '@rollup/rollup-linux-s390x-gnu@4.53.3':
+ '@rollup/rollup-linux-s390x-gnu@4.53.5':
optional: true
- '@rollup/rollup-linux-x64-gnu@4.53.3':
+ '@rollup/rollup-linux-x64-gnu@4.53.5':
optional: true
- '@rollup/rollup-linux-x64-musl@4.53.3':
+ '@rollup/rollup-linux-x64-musl@4.53.5':
optional: true
- '@rollup/rollup-openharmony-arm64@4.53.3':
+ '@rollup/rollup-openharmony-arm64@4.53.5':
optional: true
- '@rollup/rollup-win32-arm64-msvc@4.53.3':
+ '@rollup/rollup-win32-arm64-msvc@4.53.5':
optional: true
- '@rollup/rollup-win32-ia32-msvc@4.53.3':
+ '@rollup/rollup-win32-ia32-msvc@4.53.5':
optional: true
- '@rollup/rollup-win32-x64-gnu@4.53.3':
+ '@rollup/rollup-win32-x64-gnu@4.53.5':
optional: true
- '@rollup/rollup-win32-x64-msvc@4.53.3':
+ '@rollup/rollup-win32-x64-msvc@4.53.5':
optional: true
'@sindresorhus/base62@1.0.0': {}
@@ -5971,13 +5968,13 @@ snapshots:
'@speed-highlight/core@1.2.12': {}
- '@standard-schema/spec@1.0.0': {}
+ '@standard-schema/spec@1.1.0': {}
- '@stylistic/eslint-plugin@5.6.1(eslint@9.39.1(jiti@2.6.1))':
+ '@stylistic/eslint-plugin@5.6.1(eslint@9.39.2(jiti@2.6.1))':
dependencies:
- '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
- '@typescript-eslint/types': 8.48.1
- eslint: 9.39.1(jiti@2.6.1)
+ '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1))
+ '@typescript-eslint/types': 8.50.0
+ eslint: 9.39.2(jiti@2.6.1)
eslint-visitor-keys: 4.2.1
espree: 10.4.0
estraverse: 5.3.0
@@ -6012,16 +6009,15 @@ snapshots:
'@types/web-bluetooth@0.0.21': {}
- '@typescript-eslint/eslint-plugin@8.48.1(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
+ '@typescript-eslint/eslint-plugin@8.50.0(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
- '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
- '@typescript-eslint/scope-manager': 8.48.1
- '@typescript-eslint/type-utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
- '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
- '@typescript-eslint/visitor-keys': 8.48.1
- eslint: 9.39.1(jiti@2.6.1)
- graphemer: 1.4.0
+ '@typescript-eslint/parser': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/scope-manager': 8.50.0
+ '@typescript-eslint/type-utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/visitor-keys': 8.50.0
+ eslint: 9.39.2(jiti@2.6.1)
ignore: 7.0.5
natural-compare: 1.4.0
ts-api-utils: 2.1.0(typescript@5.9.3)
@@ -6029,56 +6025,56 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
+ '@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
- '@typescript-eslint/scope-manager': 8.48.1
- '@typescript-eslint/types': 8.48.1
- '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3)
- '@typescript-eslint/visitor-keys': 8.48.1
+ '@typescript-eslint/scope-manager': 8.50.0
+ '@typescript-eslint/types': 8.50.0
+ '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3)
+ '@typescript-eslint/visitor-keys': 8.50.0
debug: 4.4.3
- eslint: 9.39.1(jiti@2.6.1)
+ eslint: 9.39.2(jiti@2.6.1)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/project-service@8.48.1(typescript@5.9.3)':
+ '@typescript-eslint/project-service@8.50.0(typescript@5.9.3)':
dependencies:
- '@typescript-eslint/tsconfig-utils': 8.48.1(typescript@5.9.3)
- '@typescript-eslint/types': 8.48.1
+ '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3)
+ '@typescript-eslint/types': 8.50.0
debug: 4.4.3
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/scope-manager@8.48.1':
+ '@typescript-eslint/scope-manager@8.50.0':
dependencies:
- '@typescript-eslint/types': 8.48.1
- '@typescript-eslint/visitor-keys': 8.48.1
+ '@typescript-eslint/types': 8.50.0
+ '@typescript-eslint/visitor-keys': 8.50.0
- '@typescript-eslint/tsconfig-utils@8.48.1(typescript@5.9.3)':
+ '@typescript-eslint/tsconfig-utils@8.50.0(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
- '@typescript-eslint/type-utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
+ '@typescript-eslint/type-utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
- '@typescript-eslint/types': 8.48.1
- '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3)
- '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/types': 8.50.0
+ '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
debug: 4.4.3
- eslint: 9.39.1(jiti@2.6.1)
+ eslint: 9.39.2(jiti@2.6.1)
ts-api-utils: 2.1.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/types@8.48.1': {}
+ '@typescript-eslint/types@8.50.0': {}
- '@typescript-eslint/typescript-estree@8.48.1(typescript@5.9.3)':
+ '@typescript-eslint/typescript-estree@8.50.0(typescript@5.9.3)':
dependencies:
- '@typescript-eslint/project-service': 8.48.1(typescript@5.9.3)
- '@typescript-eslint/tsconfig-utils': 8.48.1(typescript@5.9.3)
- '@typescript-eslint/types': 8.48.1
- '@typescript-eslint/visitor-keys': 8.48.1
+ '@typescript-eslint/project-service': 8.50.0(typescript@5.9.3)
+ '@typescript-eslint/tsconfig-utils': 8.50.0(typescript@5.9.3)
+ '@typescript-eslint/types': 8.50.0
+ '@typescript-eslint/visitor-keys': 8.50.0
debug: 4.4.3
minimatch: 9.0.5
semver: 7.7.3
@@ -6088,20 +6084,20 @@ snapshots:
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
+ '@typescript-eslint/utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
- '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
- '@typescript-eslint/scope-manager': 8.48.1
- '@typescript-eslint/types': 8.48.1
- '@typescript-eslint/typescript-estree': 8.48.1(typescript@5.9.3)
- eslint: 9.39.1(jiti@2.6.1)
+ '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1))
+ '@typescript-eslint/scope-manager': 8.50.0
+ '@typescript-eslint/types': 8.50.0
+ '@typescript-eslint/typescript-estree': 8.50.0(typescript@5.9.3)
+ eslint: 9.39.2(jiti@2.6.1)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
- '@typescript-eslint/visitor-keys@8.48.1':
+ '@typescript-eslint/visitor-keys@8.50.0':
dependencies:
- '@typescript-eslint/types': 8.48.1
+ '@typescript-eslint/types': 8.50.0
eslint-visitor-keys: 4.2.1
'@unhead/vue@2.0.19(vue@3.5.25(typescript@5.9.3))':
@@ -6169,10 +6165,10 @@ snapshots:
'@unrs/resolver-binding-win32-x64-msvc@1.11.1':
optional: true
- '@vercel/nft@0.30.4(rollup@4.53.3)':
+ '@vercel/nft@0.30.4(rollup@4.53.5)':
dependencies:
'@mapbox/node-pre-gyp': 2.0.3
- '@rollup/pluginutils': 5.3.0(rollup@4.53.3)
+ '@rollup/pluginutils': 5.3.0(rollup@4.53.5)
acorn: 8.15.0
acorn-import-attributes: 1.9.5(acorn@8.15.0)
async-sema: 3.1.1
@@ -6188,72 +6184,72 @@ snapshots:
- rollup
- supports-color
- '@vitejs/plugin-vue-jsx@5.1.2(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))':
+ '@vitejs/plugin-vue-jsx@5.1.2(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))':
dependencies:
'@babel/core': 7.28.5
'@babel/plugin-syntax-typescript': 7.27.1(@babel/core@7.28.5)
'@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.28.5)
- '@rolldown/pluginutils': 1.0.0-beta.53
+ '@rolldown/pluginutils': 1.0.0-beta.9-commit.d91dfb5
'@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.28.5)
- vite: 7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
+ vite: 7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
vue: 3.5.25(typescript@5.9.3)
transitivePeerDependencies:
- supports-color
- '@vitejs/plugin-vue@6.0.2(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))':
+ '@vitejs/plugin-vue@6.0.3(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))':
dependencies:
- '@rolldown/pluginutils': 1.0.0-beta.50
- vite: 7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
+ '@rolldown/pluginutils': 1.0.0-beta.53
+ vite: 7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
vue: 3.5.25(typescript@5.9.3)
- '@vitest/expect@4.0.15':
+ '@vitest/expect@4.0.16':
dependencies:
- '@standard-schema/spec': 1.0.0
+ '@standard-schema/spec': 1.1.0
'@types/chai': 5.2.3
- '@vitest/spy': 4.0.15
- '@vitest/utils': 4.0.15
+ '@vitest/spy': 4.0.16
+ '@vitest/utils': 4.0.16
chai: 6.2.1
tinyrainbow: 3.0.3
- '@vitest/mocker@4.0.15(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))':
+ '@vitest/mocker@4.0.16(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))':
dependencies:
- '@vitest/spy': 4.0.15
+ '@vitest/spy': 4.0.16
estree-walker: 3.0.3
magic-string: 0.30.21
optionalDependencies:
- vite: 7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
+ vite: 7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
- '@vitest/pretty-format@4.0.15':
+ '@vitest/pretty-format@4.0.16':
dependencies:
tinyrainbow: 3.0.3
- '@vitest/runner@4.0.15':
+ '@vitest/runner@4.0.16':
dependencies:
- '@vitest/utils': 4.0.15
+ '@vitest/utils': 4.0.16
pathe: 2.0.3
- '@vitest/snapshot@4.0.15':
+ '@vitest/snapshot@4.0.16':
dependencies:
- '@vitest/pretty-format': 4.0.15
+ '@vitest/pretty-format': 4.0.16
magic-string: 0.30.21
pathe: 2.0.3
- '@vitest/spy@4.0.15': {}
+ '@vitest/spy@4.0.16': {}
- '@vitest/utils@4.0.15':
+ '@vitest/utils@4.0.16':
dependencies:
- '@vitest/pretty-format': 4.0.15
+ '@vitest/pretty-format': 4.0.16
tinyrainbow: 3.0.3
- '@volar/language-core@2.4.23':
+ '@volar/language-core@2.4.26':
dependencies:
- '@volar/source-map': 2.4.23
+ '@volar/source-map': 2.4.26
- '@volar/source-map@2.4.23': {}
+ '@volar/source-map@2.4.26': {}
- '@volar/typescript@2.4.23':
+ '@volar/typescript@2.4.26':
dependencies:
- '@volar/language-core': 2.4.23
+ '@volar/language-core': 2.4.26
path-browserify: 1.0.1
vscode-uri: 3.1.0
@@ -6332,14 +6328,14 @@ snapshots:
dependencies:
'@vue/devtools-kit': 7.7.9
- '@vue/devtools-core@8.0.5(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))':
+ '@vue/devtools-core@8.0.5(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))':
dependencies:
'@vue/devtools-kit': 8.0.5
'@vue/devtools-shared': 8.0.5
mitt: 3.0.1
nanoid: 5.1.6
pathe: 2.0.3
- vite-hot-client: 2.1.0(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))
+ vite-hot-client: 2.1.0(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))
vue: 3.5.25(typescript@5.9.3)
transitivePeerDependencies:
- vite
@@ -6372,9 +6368,9 @@ snapshots:
dependencies:
rfdc: 1.4.1
- '@vue/language-core@3.1.5(typescript@5.9.3)':
+ '@vue/language-core@3.1.8(typescript@5.9.3)':
dependencies:
- '@volar/language-core': 2.4.23
+ '@volar/language-core': 2.4.26
'@vue/compiler-dom': 3.5.25
'@vue/shared': 3.5.25
alien-signals: 3.1.1
@@ -6417,13 +6413,13 @@ snapshots:
'@vueuse/metadata@14.1.0': {}
- '@vueuse/nuxt@14.1.0(magicast@0.5.1)(nuxt@4.2.1(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.25)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.3)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.5(typescript@5.9.3))(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))':
+ '@vueuse/nuxt@14.1.0(magicast@0.5.1)(nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.25)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.5)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.8(typescript@5.9.3))(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))':
dependencies:
- '@nuxt/kit': 4.2.1(magicast@0.5.1)
+ '@nuxt/kit': 4.2.2(magicast@0.5.1)
'@vueuse/core': 14.1.0(vue@3.5.25(typescript@5.9.3))
'@vueuse/metadata': 14.1.0
local-pkg: 1.1.2
- nuxt: 4.2.1(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.25)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.3)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.5(typescript@5.9.3))(yaml@2.8.2)
+ nuxt: 4.2.2(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.25)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.5)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.8(typescript@5.9.3))(yaml@2.8.2)
vue: 3.5.25(typescript@5.9.3)
transitivePeerDependencies:
- magicast
@@ -6525,12 +6521,11 @@ snapshots:
async@3.2.6: {}
- autoprefixer@10.4.22(postcss@8.5.6):
+ autoprefixer@10.4.23(postcss@8.5.6):
dependencies:
browserslist: 4.28.1
- caniuse-lite: 1.0.30001759
+ caniuse-lite: 1.0.30001760
fraction.js: 5.3.4
- normalize-range: 0.1.2
picocolors: 1.1.1
postcss: 8.5.6
postcss-value-parser: 4.2.0
@@ -6543,7 +6538,7 @@ snapshots:
base64-js@1.5.1: {}
- baseline-browser-mapping@2.9.2: {}
+ baseline-browser-mapping@2.9.8: {}
bidi-js@1.0.3:
dependencies:
@@ -6573,11 +6568,11 @@ snapshots:
browserslist@4.28.1:
dependencies:
- baseline-browser-mapping: 2.9.2
- caniuse-lite: 1.0.30001759
- electron-to-chromium: 1.5.266
+ baseline-browser-mapping: 2.9.8
+ caniuse-lite: 1.0.30001760
+ electron-to-chromium: 1.5.267
node-releases: 2.0.27
- update-browserslist-db: 1.2.2(browserslist@4.28.1)
+ update-browserslist-db: 1.2.3(browserslist@4.28.1)
buffer-crc32@1.0.0: {}
@@ -6623,11 +6618,11 @@ snapshots:
caniuse-api@3.0.0:
dependencies:
browserslist: 4.28.1
- caniuse-lite: 1.0.30001759
+ caniuse-lite: 1.0.30001760
lodash.memoize: 4.1.2
lodash.uniq: 4.5.0
- caniuse-lite@1.0.30001759: {}
+ caniuse-lite@1.0.30001760: {}
chai@6.2.1: {}
@@ -6825,11 +6820,13 @@ snapshots:
dependencies:
css-tree: 2.2.1
- cssstyle@5.3.3:
+ cssstyle@5.3.4(postcss@8.5.6):
dependencies:
- '@asamuzakjp/css-color': 4.1.0
- '@csstools/css-syntax-patches-for-csstree': 1.0.20
+ '@asamuzakjp/css-color': 4.1.1
+ '@csstools/css-syntax-patches-for-csstree': 1.0.14(postcss@8.5.6)
css-tree: 3.1.0
+ transitivePeerDependencies:
+ - postcss
optional: true
csstype@3.2.3: {}
@@ -6880,7 +6877,7 @@ snapshots:
detect-libc@2.1.2: {}
- devalue@5.5.0: {}
+ devalue@5.6.1: {}
diff@8.0.2: {}
@@ -6904,7 +6901,7 @@ snapshots:
dot-prop@10.1.0:
dependencies:
- type-fest: 5.3.0
+ type-fest: 5.3.1
dotenv@16.6.1: {}
@@ -6916,7 +6913,7 @@ snapshots:
ee-first@1.1.1: {}
- electron-to-chromium@1.5.266: {}
+ electron-to-chromium@1.5.267: {}
emoji-regex@8.0.0: {}
@@ -7017,10 +7014,10 @@ snapshots:
escape-string-regexp@5.0.0: {}
- eslint-config-flat-gitignore@2.1.0(eslint@9.39.1(jiti@2.6.1)):
+ eslint-config-flat-gitignore@2.1.0(eslint@9.39.2(jiti@2.6.1)):
dependencies:
- '@eslint/compat': 1.4.1(eslint@9.39.1(jiti@2.6.1))
- eslint: 9.39.1(jiti@2.6.1)
+ '@eslint/compat': 1.4.1(eslint@9.39.2(jiti@2.6.1))
+ eslint: 9.39.2(jiti@2.6.1)
eslint-flat-config-utils@2.1.4:
dependencies:
@@ -7033,24 +7030,24 @@ snapshots:
optionalDependencies:
unrs-resolver: 1.11.1
- eslint-merge-processors@2.0.0(eslint@9.39.1(jiti@2.6.1)):
+ eslint-merge-processors@2.0.0(eslint@9.39.2(jiti@2.6.1)):
dependencies:
- eslint: 9.39.1(jiti@2.6.1)
+ eslint: 9.39.2(jiti@2.6.1)
- eslint-plugin-import-lite@0.3.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3):
+ eslint-plugin-import-lite@0.3.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3):
dependencies:
- '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
- '@typescript-eslint/types': 8.48.1
- eslint: 9.39.1(jiti@2.6.1)
+ '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1))
+ '@typescript-eslint/types': 8.50.0
+ eslint: 9.39.2(jiti@2.6.1)
optionalDependencies:
typescript: 5.9.3
- eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1)):
+ eslint-plugin-import-x@4.16.1(@typescript-eslint/utils@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1)):
dependencies:
- '@typescript-eslint/types': 8.48.1
+ '@typescript-eslint/types': 8.50.0
comment-parser: 1.4.1
debug: 4.4.3
- eslint: 9.39.1(jiti@2.6.1)
+ eslint: 9.39.2(jiti@2.6.1)
eslint-import-context: 0.1.9(unrs-resolver@1.11.1)
is-glob: 4.0.3
minimatch: 10.1.1
@@ -7058,11 +7055,11 @@ snapshots:
stable-hash-x: 0.2.0
unrs-resolver: 1.11.1
optionalDependencies:
- '@typescript-eslint/utils': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
+ '@typescript-eslint/utils': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
transitivePeerDependencies:
- supports-color
- eslint-plugin-jsdoc@61.4.1(eslint@9.39.1(jiti@2.6.1)):
+ eslint-plugin-jsdoc@61.5.0(eslint@9.39.2(jiti@2.6.1)):
dependencies:
'@es-joy/jsdoccomment': 0.76.0
'@es-joy/resolve.exports': 1.2.0
@@ -7070,7 +7067,7 @@ snapshots:
comment-parser: 1.4.1
debug: 4.4.3
escape-string-regexp: 4.0.0
- eslint: 9.39.1(jiti@2.6.1)
+ eslint: 9.39.2(jiti@2.6.1)
espree: 10.4.0
esquery: 1.6.0
html-entities: 2.6.0
@@ -7082,27 +7079,27 @@ snapshots:
transitivePeerDependencies:
- supports-color
- eslint-plugin-regexp@2.10.0(eslint@9.39.1(jiti@2.6.1)):
+ eslint-plugin-regexp@2.10.0(eslint@9.39.2(jiti@2.6.1)):
dependencies:
- '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
+ '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1))
'@eslint-community/regexpp': 4.12.2
comment-parser: 1.4.1
- eslint: 9.39.1(jiti@2.6.1)
+ eslint: 9.39.2(jiti@2.6.1)
jsdoc-type-pratt-parser: 4.8.0
refa: 0.12.1
regexp-ast-analysis: 0.7.1
scslre: 0.3.0
- eslint-plugin-unicorn@62.0.0(eslint@9.39.1(jiti@2.6.1)):
+ eslint-plugin-unicorn@62.0.0(eslint@9.39.2(jiti@2.6.1)):
dependencies:
'@babel/helper-validator-identifier': 7.28.5
- '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
+ '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1))
'@eslint/plugin-kit': 0.4.1
change-case: 5.4.4
ci-info: 4.3.1
clean-regexp: 1.0.0
core-js-compat: 3.47.0
- eslint: 9.39.1(jiti@2.6.1)
+ eslint: 9.39.2(jiti@2.6.1)
esquery: 1.6.0
find-up-simple: 1.0.1
globals: 16.5.0
@@ -7115,33 +7112,33 @@ snapshots:
semver: 7.7.3
strip-indent: 4.1.1
- eslint-plugin-vue@10.6.2(@stylistic/eslint-plugin@5.6.1(eslint@9.39.1(jiti@2.6.1)))(@typescript-eslint/parser@8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.39.1(jiti@2.6.1))):
+ eslint-plugin-vue@10.6.2(@stylistic/eslint-plugin@5.6.1(eslint@9.39.2(jiti@2.6.1)))(@typescript-eslint/parser@8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.2(jiti@2.6.1))(vue-eslint-parser@10.2.0(eslint@9.39.2(jiti@2.6.1))):
dependencies:
- '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
- eslint: 9.39.1(jiti@2.6.1)
+ '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1))
+ eslint: 9.39.2(jiti@2.6.1)
natural-compare: 1.4.0
nth-check: 2.1.1
postcss-selector-parser: 7.1.1
semver: 7.7.3
- vue-eslint-parser: 10.2.0(eslint@9.39.1(jiti@2.6.1))
+ vue-eslint-parser: 10.2.0(eslint@9.39.2(jiti@2.6.1))
xml-name-validator: 4.0.0
optionalDependencies:
- '@stylistic/eslint-plugin': 5.6.1(eslint@9.39.1(jiti@2.6.1))
- '@typescript-eslint/parser': 8.48.1(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
+ '@stylistic/eslint-plugin': 5.6.1(eslint@9.39.2(jiti@2.6.1))
+ '@typescript-eslint/parser': 8.50.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3)
- eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.25)(eslint@9.39.1(jiti@2.6.1)):
+ eslint-processor-vue-blocks@2.0.0(@vue/compiler-sfc@3.5.25)(eslint@9.39.2(jiti@2.6.1)):
dependencies:
'@vue/compiler-sfc': 3.5.25
- eslint: 9.39.1(jiti@2.6.1)
+ eslint: 9.39.2(jiti@2.6.1)
eslint-scope@8.4.0:
dependencies:
esrecurse: 4.3.0
estraverse: 5.3.0
- eslint-typegen@2.3.0(eslint@9.39.1(jiti@2.6.1)):
+ eslint-typegen@2.3.0(eslint@9.39.2(jiti@2.6.1)):
dependencies:
- eslint: 9.39.1(jiti@2.6.1)
+ eslint: 9.39.2(jiti@2.6.1)
json-schema-to-typescript-lite: 15.0.0
ohash: 2.0.11
@@ -7149,15 +7146,15 @@ snapshots:
eslint-visitor-keys@4.2.1: {}
- eslint@9.39.1(jiti@2.6.1):
+ eslint@9.39.2(jiti@2.6.1):
dependencies:
- '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
+ '@eslint-community/eslint-utils': 4.9.0(eslint@9.39.2(jiti@2.6.1))
'@eslint-community/regexpp': 4.12.2
'@eslint/config-array': 0.21.1
'@eslint/config-helpers': 0.4.2
'@eslint/core': 0.17.0
'@eslint/eslintrc': 3.3.3
- '@eslint/js': 9.39.1
+ '@eslint/js': 9.39.2
'@eslint/plugin-kit': 0.4.1
'@humanfs/node': 0.16.7
'@humanwhocodes/module-importer': 1.0.1
@@ -7238,7 +7235,7 @@ snapshots:
signal-exit: 4.1.0
strip-final-newline: 3.0.0
- expect-type@1.2.2: {}
+ expect-type@1.3.0: {}
exsolve@1.0.8: {}
@@ -7297,13 +7294,13 @@ snapshots:
flatted@3.3.3: {}
- floating-vue@5.2.2(@nuxt/kit@3.20.1(magicast@0.5.1))(vue@3.5.25(typescript@5.9.3)):
+ floating-vue@5.2.2(@nuxt/kit@3.20.2(magicast@0.5.1))(vue@3.5.25(typescript@5.9.3)):
dependencies:
'@floating-ui/dom': 1.1.1
vue: 3.5.25(typescript@5.9.3)
vue-resize: 2.0.0-alpha.1(vue@3.5.25(typescript@5.9.3))
optionalDependencies:
- '@nuxt/kit': 3.20.1(magicast@0.5.1)
+ '@nuxt/kit': 3.20.2(magicast@0.5.1)
foreground-child@3.3.1:
dependencies:
@@ -7389,8 +7386,6 @@ snapshots:
graceful-fs@4.2.11: {}
- graphemer@1.4.0: {}
-
gzip-size@7.0.0:
dependencies:
duplexer: 0.1.2
@@ -7597,10 +7592,10 @@ snapshots:
jsdoc-type-pratt-parser@6.10.0: {}
- jsdom@27.0.0:
+ jsdom@27.0.0(postcss@8.5.6):
dependencies:
- '@asamuzakjp/dom-selector': 6.7.5
- cssstyle: 5.3.3
+ '@asamuzakjp/dom-selector': 6.7.6
+ cssstyle: 5.3.4(postcss@8.5.6)
data-urls: 6.0.0
decimal.js: 10.6.0
html-encoding-sniffer: 4.0.0
@@ -7621,6 +7616,7 @@ snapshots:
xml-name-validator: 5.0.0
transitivePeerDependencies:
- bufferutil
+ - postcss
- supports-color
- utf-8-validate
optional: true
@@ -7846,14 +7842,14 @@ snapshots:
nitropack@2.12.9:
dependencies:
'@cloudflare/kv-asset-handler': 0.4.1
- '@rollup/plugin-alias': 5.1.1(rollup@4.53.3)
- '@rollup/plugin-commonjs': 28.0.9(rollup@4.53.3)
- '@rollup/plugin-inject': 5.0.5(rollup@4.53.3)
- '@rollup/plugin-json': 6.1.0(rollup@4.53.3)
- '@rollup/plugin-node-resolve': 16.0.3(rollup@4.53.3)
- '@rollup/plugin-replace': 6.0.3(rollup@4.53.3)
- '@rollup/plugin-terser': 0.4.4(rollup@4.53.3)
- '@vercel/nft': 0.30.4(rollup@4.53.3)
+ '@rollup/plugin-alias': 5.1.1(rollup@4.53.5)
+ '@rollup/plugin-commonjs': 28.0.9(rollup@4.53.5)
+ '@rollup/plugin-inject': 5.0.5(rollup@4.53.5)
+ '@rollup/plugin-json': 6.1.0(rollup@4.53.5)
+ '@rollup/plugin-node-resolve': 16.0.3(rollup@4.53.5)
+ '@rollup/plugin-replace': 6.0.3(rollup@4.53.5)
+ '@rollup/plugin-terser': 0.4.4(rollup@4.53.5)
+ '@vercel/nft': 0.30.4(rollup@4.53.5)
archiver: 7.0.1
c12: 3.3.2(magicast@0.5.1)
chokidar: 4.0.3
@@ -7895,12 +7891,12 @@ snapshots:
pkg-types: 2.3.0
pretty-bytes: 7.1.0
radix3: 1.1.2
- rollup: 4.53.3
- rollup-plugin-visualizer: 6.0.5(rollup@4.53.3)
+ rollup: 4.53.5
+ rollup-plugin-visualizer: 6.0.5(rollup@4.53.5)
scule: 1.3.0
semver: 7.7.3
serve-placeholder: 2.0.2
- serve-static: 2.2.0
+ serve-static: 2.2.1
source-map: 0.7.6
std-env: 3.10.0
ufo: 1.6.1
@@ -7908,7 +7904,7 @@ snapshots:
uncrypto: 0.1.3
unctx: 2.4.1
unenv: 2.0.0-rc.24
- unimport: 5.5.0
+ unimport: 5.6.0
unplugin-utils: 0.3.1
unstorage: 1.17.3(db0@0.3.4)(ioredis@5.8.2)
untyped: 2.0.0
@@ -7967,8 +7963,6 @@ snapshots:
normalize-path@3.0.0: {}
- normalize-range@0.1.2: {}
-
npm-run-path@5.3.0:
dependencies:
path-key: 4.0.0
@@ -7982,26 +7976,26 @@ snapshots:
dependencies:
boolbase: 1.0.0
- nuxt@4.2.1(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.25)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.3)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.5(typescript@5.9.3))(yaml@2.8.2):
+ nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.25)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.5)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.8(typescript@5.9.3))(yaml@2.8.2):
dependencies:
'@dxup/nuxt': 0.2.2(magicast@0.5.1)
- '@nuxt/cli': 3.31.1(cac@6.7.14)(magicast@0.5.1)
- '@nuxt/devtools': 3.1.1(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))
- '@nuxt/kit': 4.2.1(magicast@0.5.1)
- '@nuxt/nitro-server': 4.2.1(db0@0.3.4)(ioredis@5.8.2)(magicast@0.5.1)(nuxt@4.2.1(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.25)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.3)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.5(typescript@5.9.3))(yaml@2.8.2))(typescript@5.9.3)
- '@nuxt/schema': 4.2.1
+ '@nuxt/cli': 3.31.2(cac@6.7.14)(magicast@0.5.1)
+ '@nuxt/devtools': 3.1.1(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3))
+ '@nuxt/kit': 4.2.2(magicast@0.5.1)
+ '@nuxt/nitro-server': 4.2.2(db0@0.3.4)(ioredis@5.8.2)(magicast@0.5.1)(nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.25)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.5)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.8(typescript@5.9.3))(yaml@2.8.2))(typescript@5.9.3)
+ '@nuxt/schema': 4.2.2
'@nuxt/telemetry': 2.6.6(magicast@0.5.1)
- '@nuxt/vite-builder': 4.2.1(@types/node@22.18.1)(eslint@9.39.1(jiti@2.6.1))(magicast@0.5.1)(nuxt@4.2.1(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.25)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.1(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.3)(terser@5.44.1)(typescript@5.9.3)(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.5(typescript@5.9.3))(yaml@2.8.2))(optionator@0.9.4)(rollup@4.53.3)(terser@5.44.1)(typescript@5.9.3)(vue-tsc@3.1.5(typescript@5.9.3))(vue@3.5.25(typescript@5.9.3))(yaml@2.8.2)
+ '@nuxt/vite-builder': 4.2.2(@types/node@22.18.1)(eslint@9.39.2(jiti@2.6.1))(magicast@0.5.1)(nuxt@4.2.2(@parcel/watcher@2.5.1)(@types/node@22.18.1)(@vue/compiler-sfc@3.5.25)(cac@6.7.14)(db0@0.3.4)(eslint@9.39.2(jiti@2.6.1))(ioredis@5.8.2)(magicast@0.5.1)(optionator@0.9.4)(rollup@4.53.5)(terser@5.44.1)(typescript@5.9.3)(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.8(typescript@5.9.3))(yaml@2.8.2))(optionator@0.9.4)(rollup@4.53.5)(terser@5.44.1)(typescript@5.9.3)(vue-tsc@3.1.8(typescript@5.9.3))(vue@3.5.25(typescript@5.9.3))(yaml@2.8.2)
'@unhead/vue': 2.0.19(vue@3.5.25(typescript@5.9.3))
'@vue/shared': 3.5.25
c12: 3.3.2(magicast@0.5.1)
- chokidar: 4.0.3
+ chokidar: 5.0.0
compatx: 0.2.0
consola: 3.4.2
cookie-es: 2.0.0
defu: 6.1.4
destr: 2.0.5
- devalue: 5.5.0
+ devalue: 5.6.1
errx: 0.1.0
escape-string-regexp: 5.0.0
exsolve: 1.0.8
@@ -8019,10 +8013,10 @@ snapshots:
ofetch: 1.5.1
ohash: 2.0.11
on-change: 6.0.1
- oxc-minify: 0.96.0
- oxc-parser: 0.96.0
- oxc-transform: 0.96.0
- oxc-walker: 0.5.2(oxc-parser@0.96.0)
+ oxc-minify: 0.102.0
+ oxc-parser: 0.102.0
+ oxc-transform: 0.102.0
+ oxc-walker: 0.6.0(oxc-parser@0.102.0)
pathe: 2.0.3
perfect-debounce: 2.0.0
pkg-types: 2.3.0
@@ -8035,12 +8029,12 @@ snapshots:
ultrahtml: 1.6.0
uncrypto: 0.1.3
unctx: 2.4.1
- unimport: 5.5.0
+ unimport: 5.6.0
unplugin: 2.3.11
- unplugin-vue-router: 0.16.2(@vue/compiler-sfc@3.5.25)(typescript@5.9.3)(vue-router@4.6.3(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))
+ unplugin-vue-router: 0.19.1(@vue/compiler-sfc@3.5.25)(typescript@5.9.3)(vue-router@4.6.4(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3))
untyped: 2.0.0
vue: 3.5.25(typescript@5.9.3)
- vue-router: 4.6.3(vue@3.5.25(typescript@5.9.3))
+ vue-router: 4.6.4(vue@3.5.25(typescript@5.9.3))
optionalDependencies:
'@parcel/watcher': 2.5.1
'@types/node': 22.18.1
@@ -8157,66 +8151,66 @@ snapshots:
type-check: 0.4.0
word-wrap: 1.2.5
- oxc-minify@0.96.0:
+ oxc-minify@0.102.0:
optionalDependencies:
- '@oxc-minify/binding-android-arm64': 0.96.0
- '@oxc-minify/binding-darwin-arm64': 0.96.0
- '@oxc-minify/binding-darwin-x64': 0.96.0
- '@oxc-minify/binding-freebsd-x64': 0.96.0
- '@oxc-minify/binding-linux-arm-gnueabihf': 0.96.0
- '@oxc-minify/binding-linux-arm-musleabihf': 0.96.0
- '@oxc-minify/binding-linux-arm64-gnu': 0.96.0
- '@oxc-minify/binding-linux-arm64-musl': 0.96.0
- '@oxc-minify/binding-linux-riscv64-gnu': 0.96.0
- '@oxc-minify/binding-linux-s390x-gnu': 0.96.0
- '@oxc-minify/binding-linux-x64-gnu': 0.96.0
- '@oxc-minify/binding-linux-x64-musl': 0.96.0
- '@oxc-minify/binding-wasm32-wasi': 0.96.0
- '@oxc-minify/binding-win32-arm64-msvc': 0.96.0
- '@oxc-minify/binding-win32-x64-msvc': 0.96.0
+ '@oxc-minify/binding-android-arm64': 0.102.0
+ '@oxc-minify/binding-darwin-arm64': 0.102.0
+ '@oxc-minify/binding-darwin-x64': 0.102.0
+ '@oxc-minify/binding-freebsd-x64': 0.102.0
+ '@oxc-minify/binding-linux-arm-gnueabihf': 0.102.0
+ '@oxc-minify/binding-linux-arm64-gnu': 0.102.0
+ '@oxc-minify/binding-linux-arm64-musl': 0.102.0
+ '@oxc-minify/binding-linux-riscv64-gnu': 0.102.0
+ '@oxc-minify/binding-linux-s390x-gnu': 0.102.0
+ '@oxc-minify/binding-linux-x64-gnu': 0.102.0
+ '@oxc-minify/binding-linux-x64-musl': 0.102.0
+ '@oxc-minify/binding-openharmony-arm64': 0.102.0
+ '@oxc-minify/binding-wasm32-wasi': 0.102.0
+ '@oxc-minify/binding-win32-arm64-msvc': 0.102.0
+ '@oxc-minify/binding-win32-x64-msvc': 0.102.0
- oxc-parser@0.96.0:
+ oxc-parser@0.102.0:
dependencies:
- '@oxc-project/types': 0.96.0
+ '@oxc-project/types': 0.102.0
optionalDependencies:
- '@oxc-parser/binding-android-arm64': 0.96.0
- '@oxc-parser/binding-darwin-arm64': 0.96.0
- '@oxc-parser/binding-darwin-x64': 0.96.0
- '@oxc-parser/binding-freebsd-x64': 0.96.0
- '@oxc-parser/binding-linux-arm-gnueabihf': 0.96.0
- '@oxc-parser/binding-linux-arm-musleabihf': 0.96.0
- '@oxc-parser/binding-linux-arm64-gnu': 0.96.0
- '@oxc-parser/binding-linux-arm64-musl': 0.96.0
- '@oxc-parser/binding-linux-riscv64-gnu': 0.96.0
- '@oxc-parser/binding-linux-s390x-gnu': 0.96.0
- '@oxc-parser/binding-linux-x64-gnu': 0.96.0
- '@oxc-parser/binding-linux-x64-musl': 0.96.0
- '@oxc-parser/binding-wasm32-wasi': 0.96.0
- '@oxc-parser/binding-win32-arm64-msvc': 0.96.0
- '@oxc-parser/binding-win32-x64-msvc': 0.96.0
+ '@oxc-parser/binding-android-arm64': 0.102.0
+ '@oxc-parser/binding-darwin-arm64': 0.102.0
+ '@oxc-parser/binding-darwin-x64': 0.102.0
+ '@oxc-parser/binding-freebsd-x64': 0.102.0
+ '@oxc-parser/binding-linux-arm-gnueabihf': 0.102.0
+ '@oxc-parser/binding-linux-arm64-gnu': 0.102.0
+ '@oxc-parser/binding-linux-arm64-musl': 0.102.0
+ '@oxc-parser/binding-linux-riscv64-gnu': 0.102.0
+ '@oxc-parser/binding-linux-s390x-gnu': 0.102.0
+ '@oxc-parser/binding-linux-x64-gnu': 0.102.0
+ '@oxc-parser/binding-linux-x64-musl': 0.102.0
+ '@oxc-parser/binding-openharmony-arm64': 0.102.0
+ '@oxc-parser/binding-wasm32-wasi': 0.102.0
+ '@oxc-parser/binding-win32-arm64-msvc': 0.102.0
+ '@oxc-parser/binding-win32-x64-msvc': 0.102.0
- oxc-transform@0.96.0:
+ oxc-transform@0.102.0:
optionalDependencies:
- '@oxc-transform/binding-android-arm64': 0.96.0
- '@oxc-transform/binding-darwin-arm64': 0.96.0
- '@oxc-transform/binding-darwin-x64': 0.96.0
- '@oxc-transform/binding-freebsd-x64': 0.96.0
- '@oxc-transform/binding-linux-arm-gnueabihf': 0.96.0
- '@oxc-transform/binding-linux-arm-musleabihf': 0.96.0
- '@oxc-transform/binding-linux-arm64-gnu': 0.96.0
- '@oxc-transform/binding-linux-arm64-musl': 0.96.0
- '@oxc-transform/binding-linux-riscv64-gnu': 0.96.0
- '@oxc-transform/binding-linux-s390x-gnu': 0.96.0
- '@oxc-transform/binding-linux-x64-gnu': 0.96.0
- '@oxc-transform/binding-linux-x64-musl': 0.96.0
- '@oxc-transform/binding-wasm32-wasi': 0.96.0
- '@oxc-transform/binding-win32-arm64-msvc': 0.96.0
- '@oxc-transform/binding-win32-x64-msvc': 0.96.0
+ '@oxc-transform/binding-android-arm64': 0.102.0
+ '@oxc-transform/binding-darwin-arm64': 0.102.0
+ '@oxc-transform/binding-darwin-x64': 0.102.0
+ '@oxc-transform/binding-freebsd-x64': 0.102.0
+ '@oxc-transform/binding-linux-arm-gnueabihf': 0.102.0
+ '@oxc-transform/binding-linux-arm64-gnu': 0.102.0
+ '@oxc-transform/binding-linux-arm64-musl': 0.102.0
+ '@oxc-transform/binding-linux-riscv64-gnu': 0.102.0
+ '@oxc-transform/binding-linux-s390x-gnu': 0.102.0
+ '@oxc-transform/binding-linux-x64-gnu': 0.102.0
+ '@oxc-transform/binding-linux-x64-musl': 0.102.0
+ '@oxc-transform/binding-openharmony-arm64': 0.102.0
+ '@oxc-transform/binding-wasm32-wasi': 0.102.0
+ '@oxc-transform/binding-win32-arm64-msvc': 0.102.0
+ '@oxc-transform/binding-win32-x64-msvc': 0.102.0
- oxc-walker@0.5.2(oxc-parser@0.96.0):
+ oxc-walker@0.6.0(oxc-parser@0.102.0):
dependencies:
magic-regexp: 0.10.0
- oxc-parser: 0.96.0
+ oxc-parser: 0.102.0
p-limit@3.1.0:
dependencies:
@@ -8582,41 +8576,41 @@ snapshots:
rfdc@1.4.1: {}
- rollup-plugin-visualizer@6.0.5(rollup@4.53.3):
+ rollup-plugin-visualizer@6.0.5(rollup@4.53.5):
dependencies:
open: 8.4.2
picomatch: 4.0.3
source-map: 0.7.6
yargs: 17.7.2
optionalDependencies:
- rollup: 4.53.3
+ rollup: 4.53.5
- rollup@4.53.3:
+ rollup@4.53.5:
dependencies:
'@types/estree': 1.0.8
optionalDependencies:
- '@rollup/rollup-android-arm-eabi': 4.53.3
- '@rollup/rollup-android-arm64': 4.53.3
- '@rollup/rollup-darwin-arm64': 4.53.3
- '@rollup/rollup-darwin-x64': 4.53.3
- '@rollup/rollup-freebsd-arm64': 4.53.3
- '@rollup/rollup-freebsd-x64': 4.53.3
- '@rollup/rollup-linux-arm-gnueabihf': 4.53.3
- '@rollup/rollup-linux-arm-musleabihf': 4.53.3
- '@rollup/rollup-linux-arm64-gnu': 4.53.3
- '@rollup/rollup-linux-arm64-musl': 4.53.3
- '@rollup/rollup-linux-loong64-gnu': 4.53.3
- '@rollup/rollup-linux-ppc64-gnu': 4.53.3
- '@rollup/rollup-linux-riscv64-gnu': 4.53.3
- '@rollup/rollup-linux-riscv64-musl': 4.53.3
- '@rollup/rollup-linux-s390x-gnu': 4.53.3
- '@rollup/rollup-linux-x64-gnu': 4.53.3
- '@rollup/rollup-linux-x64-musl': 4.53.3
- '@rollup/rollup-openharmony-arm64': 4.53.3
- '@rollup/rollup-win32-arm64-msvc': 4.53.3
- '@rollup/rollup-win32-ia32-msvc': 4.53.3
- '@rollup/rollup-win32-x64-gnu': 4.53.3
- '@rollup/rollup-win32-x64-msvc': 4.53.3
+ '@rollup/rollup-android-arm-eabi': 4.53.5
+ '@rollup/rollup-android-arm64': 4.53.5
+ '@rollup/rollup-darwin-arm64': 4.53.5
+ '@rollup/rollup-darwin-x64': 4.53.5
+ '@rollup/rollup-freebsd-arm64': 4.53.5
+ '@rollup/rollup-freebsd-x64': 4.53.5
+ '@rollup/rollup-linux-arm-gnueabihf': 4.53.5
+ '@rollup/rollup-linux-arm-musleabihf': 4.53.5
+ '@rollup/rollup-linux-arm64-gnu': 4.53.5
+ '@rollup/rollup-linux-arm64-musl': 4.53.5
+ '@rollup/rollup-linux-loong64-gnu': 4.53.5
+ '@rollup/rollup-linux-ppc64-gnu': 4.53.5
+ '@rollup/rollup-linux-riscv64-gnu': 4.53.5
+ '@rollup/rollup-linux-riscv64-musl': 4.53.5
+ '@rollup/rollup-linux-s390x-gnu': 4.53.5
+ '@rollup/rollup-linux-x64-gnu': 4.53.5
+ '@rollup/rollup-linux-x64-musl': 4.53.5
+ '@rollup/rollup-openharmony-arm64': 4.53.5
+ '@rollup/rollup-win32-arm64-msvc': 4.53.5
+ '@rollup/rollup-win32-ia32-msvc': 4.53.5
+ '@rollup/rollup-win32-x64-gnu': 4.53.5
+ '@rollup/rollup-win32-x64-msvc': 4.53.5
fsevents: 2.3.3
rrweb-cssom@0.8.0:
@@ -8653,7 +8647,7 @@ snapshots:
semver@7.7.3: {}
- send@1.2.0:
+ send@1.2.1:
dependencies:
debug: 4.4.3
encodeurl: 2.0.0
@@ -8679,12 +8673,12 @@ snapshots:
dependencies:
defu: 6.1.4
- serve-static@2.2.0:
+ serve-static@2.2.1:
dependencies:
encodeurl: 2.0.0
escape-html: 1.0.3
parseurl: 1.3.3
- send: 1.2.0
+ send: 1.2.1
transitivePeerDependencies:
- supports-color
@@ -8762,7 +8756,7 @@ snapshots:
speakingurl@14.0.1: {}
- srvx@0.9.7: {}
+ srvx@0.9.8: {}
stable-hash-x@0.2.0: {}
@@ -8945,7 +8939,7 @@ snapshots:
dependencies:
prelude-ls: 1.2.1
- type-fest@5.3.0:
+ type-fest@5.3.1:
dependencies:
tagged-tag: 1.0.0
@@ -8979,7 +8973,7 @@ snapshots:
unicorn-magic@0.3.0: {}
- unimport@5.5.0:
+ unimport@5.6.0:
dependencies:
acorn: 8.15.0
escape-string-regexp: 5.0.0
@@ -9006,14 +9000,14 @@ snapshots:
pathe: 2.0.3
picomatch: 4.0.3
- unplugin-vue-router@0.16.2(@vue/compiler-sfc@3.5.25)(typescript@5.9.3)(vue-router@4.6.3(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3)):
+ unplugin-vue-router@0.19.1(@vue/compiler-sfc@3.5.25)(typescript@5.9.3)(vue-router@4.6.4(vue@3.5.25(typescript@5.9.3)))(vue@3.5.25(typescript@5.9.3)):
dependencies:
'@babel/generator': 7.28.5
'@vue-macros/common': 3.1.1(vue@3.5.25(typescript@5.9.3))
'@vue/compiler-sfc': 3.5.25
- '@vue/language-core': 3.1.5(typescript@5.9.3)
+ '@vue/language-core': 3.1.8(typescript@5.9.3)
ast-walker-scope: 0.8.3
- chokidar: 4.0.3
+ chokidar: 5.0.0
json5: 2.2.3
local-pkg: 1.1.2
magic-string: 0.30.21
@@ -9027,7 +9021,7 @@ snapshots:
unplugin-utils: 0.3.1
yaml: 2.8.2
optionalDependencies:
- vue-router: 4.6.3(vue@3.5.25(typescript@5.9.3))
+ vue-router: 4.6.4(vue@3.5.25(typescript@5.9.3))
transitivePeerDependencies:
- typescript
- vue
@@ -9100,7 +9094,7 @@ snapshots:
pkg-types: 2.3.0
unplugin: 2.3.11
- update-browserslist-db@1.2.2(browserslist@4.28.1):
+ update-browserslist-db@1.2.3(browserslist@4.28.1):
dependencies:
browserslist: 4.28.1
escalade: 3.2.0
@@ -9114,15 +9108,15 @@ snapshots:
util-deprecate@1.0.2: {}
- vite-dev-rpc@1.1.0(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)):
+ vite-dev-rpc@1.1.0(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)):
dependencies:
birpc: 2.9.0
- vite: 7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
- vite-hot-client: 2.1.0(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))
+ vite: 7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
+ vite-hot-client: 2.1.0(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))
- vite-hot-client@2.1.0(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)):
+ vite-hot-client@2.1.0(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)):
dependencies:
- vite: 7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
+ vite: 7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
vite-node@5.2.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2):
dependencies:
@@ -9130,7 +9124,7 @@ snapshots:
es-module-lexer: 1.7.0
obug: 2.1.1
pathe: 2.0.3
- vite: 7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
+ vite: 7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
transitivePeerDependencies:
- '@types/node'
- jiti
@@ -9144,7 +9138,7 @@ snapshots:
- tsx
- yaml
- vite-plugin-checker@0.11.0(eslint@9.39.1(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.5(typescript@5.9.3)):
+ vite-plugin-checker@0.12.0(eslint@9.39.2(jiti@2.6.1))(optionator@0.9.4)(typescript@5.9.3)(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue-tsc@3.1.8(typescript@5.9.3)):
dependencies:
'@babel/code-frame': 7.27.1
chokidar: 4.0.3
@@ -9153,15 +9147,15 @@ snapshots:
picomatch: 4.0.3
tiny-invariant: 1.3.3
tinyglobby: 0.2.15
- vite: 7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
+ vite: 7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
vscode-uri: 3.1.0
optionalDependencies:
- eslint: 9.39.1(jiti@2.6.1)
+ eslint: 9.39.2(jiti@2.6.1)
optionator: 0.9.4
typescript: 5.9.3
- vue-tsc: 3.1.5(typescript@5.9.3)
+ vue-tsc: 3.1.8(typescript@5.9.3)
- vite-plugin-inspect@11.3.3(@nuxt/kit@4.2.1(magicast@0.5.1))(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)):
+ vite-plugin-inspect@11.3.3(@nuxt/kit@4.2.2(magicast@0.5.1))(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)):
dependencies:
ansis: 4.2.0
debug: 4.4.3
@@ -9171,30 +9165,30 @@ snapshots:
perfect-debounce: 2.0.0
sirv: 3.0.2
unplugin-utils: 0.3.1
- vite: 7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
- vite-dev-rpc: 1.1.0(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))
+ vite: 7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
+ vite-dev-rpc: 1.1.0(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))
optionalDependencies:
- '@nuxt/kit': 4.2.1(magicast@0.5.1)
+ '@nuxt/kit': 4.2.2(magicast@0.5.1)
transitivePeerDependencies:
- supports-color
- vite-plugin-vue-tracer@1.1.3(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3)):
+ vite-plugin-vue-tracer@1.2.0(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))(vue@3.5.25(typescript@5.9.3)):
dependencies:
estree-walker: 3.0.3
exsolve: 1.0.8
magic-string: 0.30.21
pathe: 2.0.3
source-map-js: 1.2.1
- vite: 7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
+ vite: 7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
vue: 3.5.25(typescript@5.9.3)
- vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2):
+ vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2):
dependencies:
- esbuild: 0.25.12
+ esbuild: 0.27.1
fdir: 6.5.0(picomatch@4.0.3)
picomatch: 4.0.3
postcss: 8.5.6
- rollup: 4.53.3
+ rollup: 4.53.5
tinyglobby: 0.2.15
optionalDependencies:
'@types/node': 22.18.1
@@ -9203,17 +9197,17 @@ snapshots:
terser: 5.44.1
yaml: 2.8.2
- vitest@4.0.15(@types/node@22.18.1)(jiti@2.6.1)(jsdom@27.0.0)(terser@5.44.1)(yaml@2.8.2):
+ vitest@4.0.16(@types/node@22.18.1)(jiti@2.6.1)(jsdom@27.0.0(postcss@8.5.6))(terser@5.44.1)(yaml@2.8.2):
dependencies:
- '@vitest/expect': 4.0.15
- '@vitest/mocker': 4.0.15(vite@7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))
- '@vitest/pretty-format': 4.0.15
- '@vitest/runner': 4.0.15
- '@vitest/snapshot': 4.0.15
- '@vitest/spy': 4.0.15
- '@vitest/utils': 4.0.15
+ '@vitest/expect': 4.0.16
+ '@vitest/mocker': 4.0.16(vite@7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2))
+ '@vitest/pretty-format': 4.0.16
+ '@vitest/runner': 4.0.16
+ '@vitest/snapshot': 4.0.16
+ '@vitest/spy': 4.0.16
+ '@vitest/utils': 4.0.16
es-module-lexer: 1.7.0
- expect-type: 1.2.2
+ expect-type: 1.3.0
magic-string: 0.30.21
obug: 2.1.1
pathe: 2.0.3
@@ -9223,11 +9217,11 @@ snapshots:
tinyexec: 1.0.2
tinyglobby: 0.2.15
tinyrainbow: 3.0.3
- vite: 7.2.6(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
+ vite: 7.3.0(@types/node@22.18.1)(jiti@2.6.1)(terser@5.44.1)(yaml@2.8.2)
why-is-node-running: 2.3.0
optionalDependencies:
'@types/node': 22.18.1
- jsdom: 27.0.0
+ jsdom: 27.0.0(postcss@8.5.6)
transitivePeerDependencies:
- jiti
- less
@@ -9249,10 +9243,10 @@ snapshots:
vue-devtools-stub@0.1.0: {}
- vue-eslint-parser@10.2.0(eslint@9.39.1(jiti@2.6.1)):
+ vue-eslint-parser@10.2.0(eslint@9.39.2(jiti@2.6.1)):
dependencies:
debug: 4.4.3
- eslint: 9.39.1(jiti@2.6.1)
+ eslint: 9.39.2(jiti@2.6.1)
eslint-scope: 8.4.0
eslint-visitor-keys: 4.2.1
espree: 10.4.0
@@ -9265,7 +9259,7 @@ snapshots:
dependencies:
vue: 3.5.25(typescript@5.9.3)
- vue-router@4.6.3(vue@3.5.25(typescript@5.9.3)):
+ vue-router@4.6.4(vue@3.5.25(typescript@5.9.3)):
dependencies:
'@vue/devtools-api': 6.6.4
vue: 3.5.25(typescript@5.9.3)
@@ -9274,10 +9268,10 @@ snapshots:
dependencies:
vue: 3.5.25(typescript@5.9.3)
- vue-tsc@3.1.5(typescript@5.9.3):
+ vue-tsc@3.1.8(typescript@5.9.3):
dependencies:
- '@volar/typescript': 2.4.23
- '@vue/language-core': 3.1.5(typescript@5.9.3)
+ '@volar/typescript': 2.4.26
+ '@vue/language-core': 3.1.8(typescript@5.9.3)
typescript: 5.9.3
vue@3.5.25(typescript@5.9.3):
@@ -9392,12 +9386,12 @@ snapshots:
youch-core@0.3.3:
dependencies:
- '@poppinss/exception': 1.2.2
+ '@poppinss/exception': 1.2.3
error-stack-parser-es: 1.0.5
youch@4.1.0-beta.13:
dependencies:
- '@poppinss/colors': 4.1.5
+ '@poppinss/colors': 4.1.6
'@poppinss/dumper': 0.6.5
'@speed-highlight/core': 1.2.12
cookie-es: 2.0.0
diff --git a/uv.lock b/uv.lock
index e0297e39..30e16942 100644
--- a/uv.lock
+++ b/uv.lock
@@ -105,6 +105,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490 },
]
+[[package]]
+name = "aiosqlite"
+version = "0.22.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/3a/0d/449c024bdabd0678ae07d804e60ed3b9786facd3add66f51eee67a0fccea/aiosqlite-0.22.0.tar.gz", hash = "sha256:7e9e52d72b319fcdeac727668975056c49720c995176dc57370935e5ba162bb9", size = 14707 }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/39/b2181148075272edfbbd6d87e6cd78cc71dca243446fa3b381fd4116950b/aiosqlite-0.22.0-py3-none-any.whl", hash = "sha256:96007fac2ce70eda3ca1bba7a3008c435258a592b8fbf2ee3eeaa36d33971a09", size = 17263 },
+]
+
[[package]]
name = "altgraph"
version = "0.17.5"
@@ -224,15 +233,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/07/6b/6e92009df3b8b7272f85a0992b306b61c34b7ea1c4776643746e61c380ac/brotlicffi-1.2.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:f139a7cdfe4ae7859513067b736eb44d19fae1186f9e99370092f6915216451b", size = 378586 },
]
-[[package]]
-name = "caribou"
-version = "0.4.1"
-source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/6c/74/de2c20c5a24811d69a2a6150d5cd841f2af21ffc65ff964a73f46738bb68/caribou-0.4.1.tar.gz", hash = "sha256:13a66fc9ef9b1c9e9ef220876d59a44ee5eff9e579655252271b7514fc0ad787", size = 14019 }
-wheels = [
- { url = "https://files.pythonhosted.org/packages/b5/91/795eccdad6abd41b3634502d0971cb696d2e5c9cede67f8b38ebe30d2b1e/caribou-0.4.1-py2.py3-none-any.whl", hash = "sha256:5c7a036584b34021011f1512620152272924e55ca87c7c805073aeef31c5baed", size = 7203 },
-]
-
[[package]]
name = "certifi"
version = "2025.11.12"
@@ -416,19 +416,19 @@ wheels = [
[[package]]
name = "debugpy"
-version = "1.8.17"
+version = "1.8.19"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/15/ad/71e708ff4ca377c4230530d6a7aa7992592648c122a2cd2b321cf8b35a76/debugpy-1.8.17.tar.gz", hash = "sha256:fd723b47a8c08892b1a16b2c6239a8b96637c62a59b94bb5dab4bac592a58a8e", size = 1644129 }
+sdist = { url = "https://files.pythonhosted.org/packages/73/75/9e12d4d42349b817cd545b89247696c67917aab907012ae5b64bbfea3199/debugpy-1.8.19.tar.gz", hash = "sha256:eea7e5987445ab0b5ed258093722d5ecb8bb72217c5c9b1e21f64efe23ddebdb", size = 1644590 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/50/76/597e5cb97d026274ba297af8d89138dfd9e695767ba0e0895edb20963f40/debugpy-1.8.17-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:857c1dd5d70042502aef1c6d1c2801211f3ea7e56f75e9c335f434afb403e464", size = 2538386 },
- { url = "https://files.pythonhosted.org/packages/5f/60/ce5c34fcdfec493701f9d1532dba95b21b2f6394147234dce21160bd923f/debugpy-1.8.17-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:3bea3b0b12f3946e098cce9b43c3c46e317b567f79570c3f43f0b96d00788088", size = 4292100 },
- { url = "https://files.pythonhosted.org/packages/e8/95/7873cf2146577ef71d2a20bf553f12df865922a6f87b9e8ee1df04f01785/debugpy-1.8.17-cp313-cp313-win32.whl", hash = "sha256:e34ee844c2f17b18556b5bbe59e1e2ff4e86a00282d2a46edab73fd7f18f4a83", size = 5277002 },
- { url = "https://files.pythonhosted.org/packages/46/11/18c79a1cee5ff539a94ec4aa290c1c069a5580fd5cfd2fb2e282f8e905da/debugpy-1.8.17-cp313-cp313-win_amd64.whl", hash = "sha256:6c5cd6f009ad4fca8e33e5238210dc1e5f42db07d4b6ab21ac7ffa904a196420", size = 5319047 },
- { url = "https://files.pythonhosted.org/packages/de/45/115d55b2a9da6de812696064ceb505c31e952c5d89c4ed1d9bb983deec34/debugpy-1.8.17-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:045290c010bcd2d82bc97aa2daf6837443cd52f6328592698809b4549babcee1", size = 2536899 },
- { url = "https://files.pythonhosted.org/packages/5a/73/2aa00c7f1f06e997ef57dc9b23d61a92120bec1437a012afb6d176585197/debugpy-1.8.17-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:b69b6bd9dba6a03632534cdf67c760625760a215ae289f7489a452af1031fe1f", size = 4268254 },
- { url = "https://files.pythonhosted.org/packages/86/b5/ed3e65c63c68a6634e3ba04bd10255c8e46ec16ebed7d1c79e4816d8a760/debugpy-1.8.17-cp314-cp314-win32.whl", hash = "sha256:5c59b74aa5630f3a5194467100c3b3d1c77898f9ab27e3f7dc5d40fc2f122670", size = 5277203 },
- { url = "https://files.pythonhosted.org/packages/b0/26/394276b71c7538445f29e792f589ab7379ae70fd26ff5577dfde71158e96/debugpy-1.8.17-cp314-cp314-win_amd64.whl", hash = "sha256:893cba7bb0f55161de4365584b025f7064e1f88913551bcd23be3260b231429c", size = 5318493 },
- { url = "https://files.pythonhosted.org/packages/b0/d0/89247ec250369fc76db477720a26b2fce7ba079ff1380e4ab4529d2fe233/debugpy-1.8.17-py2.py3-none-any.whl", hash = "sha256:60c7dca6571efe660ccb7a9508d73ca14b8796c4ed484c2002abba714226cfef", size = 5283210 },
+ { url = "https://files.pythonhosted.org/packages/71/3d/388035a31a59c26f1ecc8d86af607d0c42e20ef80074147cd07b180c4349/debugpy-1.8.19-cp313-cp313-macosx_15_0_universal2.whl", hash = "sha256:91e35db2672a0abaf325f4868fcac9c1674a0d9ad9bb8a8c849c03a5ebba3e6d", size = 2538859 },
+ { url = "https://files.pythonhosted.org/packages/4a/19/c93a0772d0962294f083dbdb113af1a7427bb632d36e5314297068f55db7/debugpy-1.8.19-cp313-cp313-manylinux_2_34_x86_64.whl", hash = "sha256:85016a73ab84dea1c1f1dcd88ec692993bcbe4532d1b49ecb5f3c688ae50c606", size = 4292575 },
+ { url = "https://files.pythonhosted.org/packages/5c/56/09e48ab796b0a77e3d7dc250f95251832b8bf6838c9632f6100c98bdf426/debugpy-1.8.19-cp313-cp313-win32.whl", hash = "sha256:b605f17e89ba0ecee994391194285fada89cee111cfcd29d6f2ee11cbdc40976", size = 5286209 },
+ { url = "https://files.pythonhosted.org/packages/fb/4e/931480b9552c7d0feebe40c73725dd7703dcc578ba9efc14fe0e6d31cfd1/debugpy-1.8.19-cp313-cp313-win_amd64.whl", hash = "sha256:c30639998a9f9cd9699b4b621942c0179a6527f083c72351f95c6ab1728d5b73", size = 5328206 },
+ { url = "https://files.pythonhosted.org/packages/f6/b9/cbec520c3a00508327476c7fce26fbafef98f412707e511eb9d19a2ef467/debugpy-1.8.19-cp314-cp314-macosx_15_0_universal2.whl", hash = "sha256:1e8c4d1bd230067bf1bbcdbd6032e5a57068638eb28b9153d008ecde288152af", size = 2537372 },
+ { url = "https://files.pythonhosted.org/packages/88/5e/cf4e4dc712a141e10d58405c58c8268554aec3c35c09cdcda7535ff13f76/debugpy-1.8.19-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:d40c016c1f538dbf1762936e3aeb43a89b965069d9f60f9e39d35d9d25e6b809", size = 4268729 },
+ { url = "https://files.pythonhosted.org/packages/82/a3/c91a087ab21f1047db328c1d3eb5d1ff0e52de9e74f9f6f6fa14cdd93d58/debugpy-1.8.19-cp314-cp314-win32.whl", hash = "sha256:0601708223fe1cd0e27c6cce67a899d92c7d68e73690211e6788a4b0e1903f5b", size = 5286388 },
+ { url = "https://files.pythonhosted.org/packages/17/b8/bfdc30b6e94f1eff09f2dc9cc1f9cd1c6cde3d996bcbd36ce2d9a4956e99/debugpy-1.8.19-cp314-cp314-win_amd64.whl", hash = "sha256:8e19a725f5d486f20e53a1dde2ab8bb2c9607c40c00a42ab646def962b41125f", size = 5327741 },
+ { url = "https://files.pythonhosted.org/packages/25/3e/e27078370414ef35fafad2c06d182110073daaeb5d3bf734b0b1eeefe452/debugpy-1.8.19-py2.py3-none-any.whl", hash = "sha256:360ffd231a780abbc414ba0f005dad409e71c78637efe8f2bd75837132a41d38", size = 5292321 },
]
[[package]]
@@ -1246,15 +1246,15 @@ wheels = [
[[package]]
name = "python-socketio"
-version = "5.15.0"
+version = "5.15.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "bidict" },
{ name = "python-engineio" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/72/a8/5f7c805dd6d0d6cba91d3ea215b4b88889d1b99b71a53c932629daba53f1/python_socketio-5.15.0.tar.gz", hash = "sha256:d0403ababb59aa12fd5adcfc933a821113f27bd77761bc1c54aad2e3191a9b69", size = 126439 }
+sdist = { url = "https://files.pythonhosted.org/packages/5c/b5/56d070ade9ae60ed90ce2cdb41da927791cdae31f1059aab4b6b60d223b3/python_socketio-5.15.1.tar.gz", hash = "sha256:54fe3e5580ea06a1b29b541e8ef32fe956846c99a76059e343e43aada754efdd", size = 127172 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/cd/fa/1ef2f8537272a2f383d72b9301c3ef66a49710b3bb7dcb2bd138cf2920d1/python_socketio-5.15.0-py3-none-any.whl", hash = "sha256:e93363102f4da6d8e7a8872bf4908b866c40f070e716aa27132891e643e2687c", size = 79451 },
+ { url = "https://files.pythonhosted.org/packages/5c/47/45a805fc1e4c3104df1193a78aeb98734497e32931efd1dfe9897c19188b/python_socketio-5.15.1-py3-none-any.whl", hash = "sha256:abc3528803563ed9a2010bc76829afe21d7a308a1e5651171fdb582d12e2ace0", size = 79561 },
]
[[package]]
@@ -1484,28 +1484,28 @@ wheels = [
[[package]]
name = "ruff"
-version = "0.14.8"
+version = "0.14.10"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/ed/d9/f7a0c4b3a2bf2556cd5d99b05372c29980249ef71e8e32669ba77428c82c/ruff-0.14.8.tar.gz", hash = "sha256:774ed0dd87d6ce925e3b8496feb3a00ac564bea52b9feb551ecd17e0a23d1eed", size = 5765385 }
+sdist = { url = "https://files.pythonhosted.org/packages/57/08/52232a877978dd8f9cf2aeddce3e611b40a63287dfca29b6b8da791f5e8d/ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4", size = 5859763 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/48/b8/9537b52010134b1d2b72870cc3f92d5fb759394094741b09ceccae183fbe/ruff-0.14.8-py3-none-linux_armv6l.whl", hash = "sha256:ec071e9c82eca417f6111fd39f7043acb53cd3fde9b1f95bbed745962e345afb", size = 13441540 },
- { url = "https://files.pythonhosted.org/packages/24/00/99031684efb025829713682012b6dd37279b1f695ed1b01725f85fd94b38/ruff-0.14.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:8cdb162a7159f4ca36ce980a18c43d8f036966e7f73f866ac8f493b75e0c27e9", size = 13669384 },
- { url = "https://files.pythonhosted.org/packages/72/64/3eb5949169fc19c50c04f28ece2c189d3b6edd57e5b533649dae6ca484fe/ruff-0.14.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e2fcbefe91f9fad0916850edf0854530c15bd1926b6b779de47e9ab619ea38f", size = 12806917 },
- { url = "https://files.pythonhosted.org/packages/c4/08/5250babb0b1b11910f470370ec0cbc67470231f7cdc033cee57d4976f941/ruff-0.14.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9d70721066a296f45786ec31916dc287b44040f553da21564de0ab4d45a869b", size = 13256112 },
- { url = "https://files.pythonhosted.org/packages/78/4c/6c588e97a8e8c2d4b522c31a579e1df2b4d003eddfbe23d1f262b1a431ff/ruff-0.14.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2c87e09b3cd9d126fc67a9ecd3b5b1d3ded2b9c7fce3f16e315346b9d05cfb52", size = 13227559 },
- { url = "https://files.pythonhosted.org/packages/23/ce/5f78cea13eda8eceac71b5f6fa6e9223df9b87bb2c1891c166d1f0dce9f1/ruff-0.14.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d62cb310c4fbcb9ee4ac023fe17f984ae1e12b8a4a02e3d21489f9a2a5f730c", size = 13896379 },
- { url = "https://files.pythonhosted.org/packages/cf/79/13de4517c4dadce9218a20035b21212a4c180e009507731f0d3b3f5df85a/ruff-0.14.8-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:1af35c2d62633d4da0521178e8a2641c636d2a7153da0bac1b30cfd4ccd91344", size = 15372786 },
- { url = "https://files.pythonhosted.org/packages/00/06/33df72b3bb42be8a1c3815fd4fae83fa2945fc725a25d87ba3e42d1cc108/ruff-0.14.8-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:25add4575ffecc53d60eed3f24b1e934493631b48ebbc6ebaf9d8517924aca4b", size = 14990029 },
- { url = "https://files.pythonhosted.org/packages/64/61/0f34927bd90925880394de0e081ce1afab66d7b3525336f5771dcf0cb46c/ruff-0.14.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4c943d847b7f02f7db4201a0600ea7d244d8a404fbb639b439e987edcf2baf9a", size = 14407037 },
- { url = "https://files.pythonhosted.org/packages/96/bc/058fe0aefc0fbf0d19614cb6d1a3e2c048f7dc77ca64957f33b12cfdc5ef/ruff-0.14.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb6e8bf7b4f627548daa1b69283dac5a296bfe9ce856703b03130732e20ddfe2", size = 14102390 },
- { url = "https://files.pythonhosted.org/packages/af/a4/e4f77b02b804546f4c17e8b37a524c27012dd6ff05855d2243b49a7d3cb9/ruff-0.14.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7aaf2974f378e6b01d1e257c6948207aec6a9b5ba53fab23d0182efb887a0e4a", size = 14230793 },
- { url = "https://files.pythonhosted.org/packages/3f/52/bb8c02373f79552e8d087cedaffad76b8892033d2876c2498a2582f09dcf/ruff-0.14.8-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e5758ca513c43ad8a4ef13f0f081f80f08008f410790f3611a21a92421ab045b", size = 13160039 },
- { url = "https://files.pythonhosted.org/packages/1f/ad/b69d6962e477842e25c0b11622548df746290cc6d76f9e0f4ed7456c2c31/ruff-0.14.8-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f74f7ba163b6e85a8d81a590363bf71618847e5078d90827749bfda1d88c9cdf", size = 13205158 },
- { url = "https://files.pythonhosted.org/packages/06/63/54f23da1315c0b3dfc1bc03fbc34e10378918a20c0b0f086418734e57e74/ruff-0.14.8-py3-none-musllinux_1_2_i686.whl", hash = "sha256:eed28f6fafcc9591994c42254f5a5c5ca40e69a30721d2ab18bb0bb3baac3ab6", size = 13469550 },
- { url = "https://files.pythonhosted.org/packages/70/7d/a4d7b1961e4903bc37fffb7ddcfaa7beb250f67d97cfd1ee1d5cddb1ec90/ruff-0.14.8-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:21d48fa744c9d1cb8d71eb0a740c4dd02751a5de9db9a730a8ef75ca34cf138e", size = 14211332 },
- { url = "https://files.pythonhosted.org/packages/5d/93/2a5063341fa17054e5c86582136e9895db773e3c2ffb770dde50a09f35f0/ruff-0.14.8-py3-none-win32.whl", hash = "sha256:15f04cb45c051159baebb0f0037f404f1dc2f15a927418f29730f411a79bc4e7", size = 13151890 },
- { url = "https://files.pythonhosted.org/packages/02/1c/65c61a0859c0add13a3e1cbb6024b42de587456a43006ca2d4fd3d1618fe/ruff-0.14.8-py3-none-win_amd64.whl", hash = "sha256:9eeb0b24242b5bbff3011409a739929f497f3fb5fe3b5698aba5e77e8c833097", size = 14537826 },
- { url = "https://files.pythonhosted.org/packages/6d/63/8b41cea3afd7f58eb64ac9251668ee0073789a3bc9ac6f816c8c6fef986d/ruff-0.14.8-py3-none-win_arm64.whl", hash = "sha256:965a582c93c63fe715fd3e3f8aa37c4b776777203d8e1d8aa3cc0c14424a4b99", size = 13634522 },
+ { url = "https://files.pythonhosted.org/packages/60/01/933704d69f3f05ee16ef11406b78881733c186fe14b6a46b05cfcaf6d3b2/ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49", size = 13527080 },
+ { url = "https://files.pythonhosted.org/packages/df/58/a0349197a7dfa603ffb7f5b0470391efa79ddc327c1e29c4851e85b09cc5/ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f", size = 13797320 },
+ { url = "https://files.pythonhosted.org/packages/7b/82/36be59f00a6082e38c23536df4e71cdbc6af8d7c707eade97fcad5c98235/ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d", size = 12918434 },
+ { url = "https://files.pythonhosted.org/packages/a6/00/45c62a7f7e34da92a25804f813ebe05c88aa9e0c25e5cb5a7d23dd7450e3/ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77", size = 13371961 },
+ { url = "https://files.pythonhosted.org/packages/40/31/a5906d60f0405f7e57045a70f2d57084a93ca7425f22e1d66904769d1628/ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a", size = 13275629 },
+ { url = "https://files.pythonhosted.org/packages/3e/60/61c0087df21894cf9d928dc04bcd4fb10e8b2e8dca7b1a276ba2155b2002/ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f", size = 14029234 },
+ { url = "https://files.pythonhosted.org/packages/44/84/77d911bee3b92348b6e5dab5a0c898d87084ea03ac5dc708f46d88407def/ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935", size = 15449890 },
+ { url = "https://files.pythonhosted.org/packages/e9/36/480206eaefa24a7ec321582dda580443a8f0671fdbf6b1c80e9c3e93a16a/ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e", size = 15123172 },
+ { url = "https://files.pythonhosted.org/packages/5c/38/68e414156015ba80cef5473d57919d27dfb62ec804b96180bafdeaf0e090/ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d", size = 14460260 },
+ { url = "https://files.pythonhosted.org/packages/b3/19/9e050c0dca8aba824d67cc0db69fb459c28d8cd3f6855b1405b3f29cc91d/ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f", size = 14229978 },
+ { url = "https://files.pythonhosted.org/packages/51/eb/e8dd1dd6e05b9e695aa9dd420f4577debdd0f87a5ff2fedda33c09e9be8c/ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f", size = 14338036 },
+ { url = "https://files.pythonhosted.org/packages/6a/12/f3e3a505db7c19303b70af370d137795fcfec136d670d5de5391e295c134/ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d", size = 13264051 },
+ { url = "https://files.pythonhosted.org/packages/08/64/8c3a47eaccfef8ac20e0484e68e0772013eb85802f8a9f7603ca751eb166/ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405", size = 13283998 },
+ { url = "https://files.pythonhosted.org/packages/12/84/534a5506f4074e5cc0529e5cd96cfc01bb480e460c7edf5af70d2bcae55e/ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60", size = 13601891 },
+ { url = "https://files.pythonhosted.org/packages/0d/1e/14c916087d8598917dbad9b2921d340f7884824ad6e9c55de948a93b106d/ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830", size = 14336660 },
+ { url = "https://files.pythonhosted.org/packages/f2/1c/d7b67ab43f30013b47c12b42d1acd354c195351a3f7a1d67f59e54227ede/ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6", size = 13196187 },
+ { url = "https://files.pythonhosted.org/packages/fb/9c/896c862e13886fae2af961bef3e6312db9ebc6adc2b156fe95e615dee8c1/ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154", size = 14661283 },
+ { url = "https://files.pythonhosted.org/packages/74/31/b0e29d572670dca3674eeee78e418f20bdf97fa8aa9ea71380885e175ca0/ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6", size = 13729839 },
]
[[package]]
@@ -1624,11 +1624,11 @@ wheels = [
[[package]]
name = "tzdata"
-version = "2025.2"
+version = "2025.3"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380 }
+sdist = { url = "https://files.pythonhosted.org/packages/5e/a7/c202b344c5ca7daf398f3b8a477eeb205cf3b6f32e7ec3a6bac0629ca975/tzdata-2025.3.tar.gz", hash = "sha256:de39c2ca5dc7b0344f2eba86f49d614019d29f060fc4ebc8a417896a620b56a7", size = 196772 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839 },
+ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521 },
]
[[package]]
@@ -1645,11 +1645,11 @@ wheels = [
[[package]]
name = "urllib3"
-version = "2.6.0"
+version = "2.6.2"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/1c/43/554c2569b62f49350597348fc3ac70f786e3c32e7f19d266e19817812dd3/urllib3-2.6.0.tar.gz", hash = "sha256:cb9bcef5a4b345d5da5d145dc3e30834f58e8018828cbc724d30b4cb7d4d49f1", size = 432585 }
+sdist = { url = "https://files.pythonhosted.org/packages/1e/24/a2a2ed9addd907787d7aa0355ba36a6cadf1768b934c652ea78acbd59dcd/urllib3-2.6.2.tar.gz", hash = "sha256:016f9c98bb7e98085cb2b4b17b87d2c702975664e4f060c6532e64d1c1a5e797", size = 432930 }
wheels = [
- { url = "https://files.pythonhosted.org/packages/56/1a/9ffe814d317c5224166b23e7c47f606d6e473712a2fad0f704ea9b99f246/urllib3-2.6.0-py3-none-any.whl", hash = "sha256:c90f7a39f716c572c4e3e58509581ebd83f9b59cced005b7db7ad2d22b0db99f", size = 131083 },
+ { url = "https://files.pythonhosted.org/packages/6d/b9/4095b668ea3678bf6a0af005527f39de12fb026516fb3df17495a733b7f8/urllib3-2.6.2-py3-none-any.whl", hash = "sha256:ec21cddfe7724fc7cb4ba4bea7aa8e2ef36f607a4bab81aa6ce42a13dc3f03dd", size = 131182 },
]
[package.optional-dependencies]
@@ -1823,13 +1823,13 @@ source = { virtual = "." }
dependencies = [
{ name = "aiocron" },
{ name = "aiohttp" },
+ { name = "aiosqlite" },
{ name = "anyio" },
{ name = "apprise" },
{ name = "async-timeout" },
{ name = "bgutil-ytdlp-pot-provider" },
{ name = "brotli" },
{ name = "brotlicffi" },
- { name = "caribou" },
{ name = "coloredlogs" },
{ name = "curl-cffi" },
{ name = "dateparser" },
@@ -1874,15 +1874,15 @@ dev = [
requires-dist = [
{ name = "aiocron", specifier = ">=1.8" },
{ name = "aiohttp", specifier = ">=3.9.3" },
+ { name = "aiosqlite", specifier = ">=0.22.0" },
{ name = "anyio" },
{ name = "apprise", specifier = ">=1.9.3" },
{ name = "async-timeout" },
{ name = "bgutil-ytdlp-pot-provider", specifier = ">=1.2.1" },
{ name = "brotli" },
{ name = "brotlicffi" },
- { name = "caribou", specifier = ">=0.3.0" },
{ name = "coloredlogs", specifier = ">=15.0.1" },
- { name = "curl-cffi", specifier = ">=0.13" },
+ { name = "curl-cffi", specifier = "==0.13" },
{ name = "dateparser", specifier = ">=1.2.1" },
{ name = "debugpy", specifier = ">=1.8.1" },
{ name = "defusedxml", specifier = ">=0.7.1" },
diff --git a/var/downloads/.gitkeep b/var/downloads/.gitkeep
deleted file mode 100644
index e69de29b..00000000