Merge pull request #525 from arabcoders/dev

This commit is contained in:
Abdulmohsen 2025-12-29 23:07:07 +03:00 committed by GitHub
commit 714b6d95ba
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 335 additions and 99 deletions

View file

@ -85,14 +85,14 @@ class YTDLP(yt_dlp.YoutubeDL):
if not (archive_id := self._make_archive_id(info_dict)):
return
assert archive_id
assert archive_id, "Expected non-empty archive ID"
self.write_debug(f"Adding to archive: {archive_id}")
self.archive.add(archive_id)
old_archive_ids = info_dict.get("_old_archive_ids", [])
if old_archive_ids and isinstance(old_archive_ids, list) and len(old_archive_ids) > 0:
for old_id in old_archive_ids:
if old_id == archive_id or not old_id.startswith("generic "):
if old_id == archive_id:
continue
self.write_debug(f"Adding to archive (old id): {old_id}")

View file

@ -1,8 +1,8 @@
from unittest.mock import MagicMock, patch
from unittest.mock import MagicMock, Mock, patch
from app.library.Utils import REMOVE_KEYS
from app.library.ytdlp import _ArchiveProxy, ytdlp_options
from app.library.ytdlp import YTDLP, _ArchiveProxy, ytdlp_options
class TestArchiveProxy:
@ -79,3 +79,200 @@ class TestYtDlpOptions:
# We expect at least one to be present (e.g., -P / --paths, etc.)
assert len(present_ignored_flags) > 0
assert all(flag_to_ignored[f] is True for f in present_ignored_flags)
class TestYTDLP:
"""Test the YTDLP class overridden methods."""
def _create_ytdlp(self, params=None):
"""Helper to create a YTDLP instance with mocked parent __init__."""
with patch("app.library.ytdlp.yt_dlp.YoutubeDL.__init__", return_value=None):
ytdlp = YTDLP(params=params)
ytdlp.params = params or {}
return ytdlp
@patch("app.library.ytdlp.yt_dlp.YoutubeDL.__init__")
def test_init_patches_download_archive_param(self, mock_super_init) -> None:
"""Test that __init__ removes download_archive before calling super, then restores it."""
mock_super_init.return_value = None
params = {"download_archive": "/tmp/archive.txt", "quiet": True}
ytdlp = YTDLP(params=params, auto_init=True)
# Set params as it would be after super().__init__
ytdlp.params = {}
# Verify super().__init__ was called without download_archive
mock_super_init.assert_called_once()
call_kwargs = mock_super_init.call_args[1]
assert "download_archive" not in call_kwargs["params"]
assert call_kwargs["params"]["quiet"] is True
assert call_kwargs["auto_init"] is True
# Our __init__ code manually sets these after super()
ytdlp.params["download_archive"] = "/tmp/archive.txt"
# Verify download_archive was restored to params
assert ytdlp.params["download_archive"] == "/tmp/archive.txt"
# Verify archive proxy was set up
assert isinstance(ytdlp.archive, _ArchiveProxy)
assert ytdlp.archive._file == "/tmp/archive.txt"
@patch("app.library.ytdlp.yt_dlp.YoutubeDL.__init__")
def test_init_handles_no_download_archive(self, mock_super_init) -> None:
"""Test __init__ works correctly when download_archive is not in params."""
mock_super_init.return_value = None
params = {"quiet": True}
ytdlp = YTDLP(params=params, auto_init=False)
# Verify super().__init__ was called with original params
mock_super_init.assert_called_once()
call_kwargs = mock_super_init.call_args[1]
assert call_kwargs["params"]["quiet"] is True
assert call_kwargs["auto_init"] is False
# Verify archive proxy is falsey
assert isinstance(ytdlp.archive, _ArchiveProxy)
assert not ytdlp.archive
@patch("app.library.ytdlp.yt_dlp.YoutubeDL.__init__")
def test_init_handles_none_params(self, mock_super_init) -> None:
"""Test __init__ handles None params gracefully."""
mock_super_init.return_value = None
ytdlp = YTDLP(params=None)
mock_super_init.assert_called_once_with(params=None, auto_init=True)
assert isinstance(ytdlp.archive, _ArchiveProxy)
assert not ytdlp.archive
@patch("app.library.ytdlp.yt_dlp.YoutubeDL._delete_downloaded_files")
def test_delete_downloaded_files_skips_when_interrupted(self, mock_super_delete) -> None:
"""Test _delete_downloaded_files skips cleanup when _interrupted is True."""
with patch("app.library.ytdlp.yt_dlp.YoutubeDL.__init__", return_value=None):
ytdlp = YTDLP(params={})
ytdlp.to_screen = Mock()
# Set interrupted flag
ytdlp._interrupted = True
result = ytdlp._delete_downloaded_files("arg1", "arg2", kwarg1="value1")
# Should not call super method
mock_super_delete.assert_not_called()
# Should show message
ytdlp.to_screen.assert_called_once_with("[info] Cancelled — skipping temp cleanup.")
# Should return None
assert result is None
@patch("app.library.ytdlp.yt_dlp.YoutubeDL._delete_downloaded_files")
def test_delete_downloaded_files_calls_super_when_not_interrupted(self, mock_super_delete) -> None:
"""Test _delete_downloaded_files calls super when not interrupted."""
mock_super_delete.return_value = "cleanup_result"
with patch("app.library.ytdlp.yt_dlp.YoutubeDL.__init__", return_value=None):
ytdlp = YTDLP(params={})
ytdlp._interrupted = False
result = ytdlp._delete_downloaded_files("arg1", kwarg1="value1")
# Should call super method with same args
mock_super_delete.assert_called_once_with("arg1", kwarg1="value1")
# Should return super's result
assert result == "cleanup_result"
def test_record_download_archive_does_nothing_without_download_archive_param(self) -> None:
"""Test record_download_archive returns early when download_archive is not set."""
ytdlp = self._create_ytdlp(params={})
ytdlp.archive = Mock()
ytdlp.record_download_archive({"id": "test123"})
# Should not interact with archive
ytdlp.archive.add.assert_not_called()
def test_record_download_archive_adds_archive_id(self) -> None:
"""Test record_download_archive adds the archive ID."""
ytdlp = self._create_ytdlp(params={"download_archive": "/tmp/archive.txt"})
ytdlp.write_debug = Mock()
ytdlp.archive = Mock()
ytdlp._make_archive_id = Mock(return_value="youtube test123")
info_dict = {"id": "test123", "ie_key": "Youtube"}
ytdlp.record_download_archive(info_dict)
# Verify _make_archive_id was called
ytdlp._make_archive_id.assert_called_once_with(info_dict)
# Verify archive.add was called with the archive_id
ytdlp.archive.add.assert_called_once_with("youtube test123")
ytdlp.write_debug.assert_called_with("Adding to archive: youtube test123")
def test_record_download_archive_handles_old_archive_ids(self) -> None:
"""Test record_download_archive adds _old_archive_ids when present."""
ytdlp = self._create_ytdlp(params={"download_archive": "/tmp/archive.txt"})
ytdlp.write_debug = Mock()
ytdlp.archive = Mock()
ytdlp._make_archive_id = Mock(return_value="youtube new123")
info_dict = {
"id": "new123",
"ie_key": "Youtube",
"_old_archive_ids": ["youtube old123", "youtube old456", "youtube new123"],
}
ytdlp.record_download_archive(info_dict)
# Should add main archive_id
assert ytdlp.archive.add.call_count == 3
calls = [call[0][0] for call in ytdlp.archive.add.call_args_list]
# First call is main archive_id
assert calls[0] == "youtube new123"
# Should add old IDs except the duplicate
assert "youtube old123" in calls
assert "youtube old456" in calls
# Should not add duplicate (youtube new123 appears only once)
assert calls.count("youtube new123") == 1
def test_record_download_archive_skips_empty_old_archive_ids(self) -> None:
"""Test record_download_archive handles empty or invalid _old_archive_ids."""
ytdlp = self._create_ytdlp(params={"download_archive": "/tmp/archive.txt"})
ytdlp.write_debug = Mock()
ytdlp.archive = Mock()
ytdlp._make_archive_id = Mock(return_value="youtube test123")
# Test with empty list
info_dict = {"id": "test123", "ie_key": "Youtube", "_old_archive_ids": []}
ytdlp.record_download_archive(info_dict)
assert ytdlp.archive.add.call_count == 1 # Only main ID
ytdlp.archive.reset_mock()
# Test with None
info_dict = {"id": "test123", "ie_key": "Youtube", "_old_archive_ids": None}
ytdlp.record_download_archive(info_dict)
assert ytdlp.archive.add.call_count == 1 # Only main ID
ytdlp.archive.reset_mock()
# Test with non-list value
info_dict = {"id": "test123", "ie_key": "Youtube", "_old_archive_ids": "not a list"}
ytdlp.record_download_archive(info_dict)
assert ytdlp.archive.add.call_count == 1 # Only main ID
def test_record_download_archive_returns_early_on_empty_archive_id(self) -> None:
"""Test record_download_archive returns early when _make_archive_id returns empty."""
ytdlp = self._create_ytdlp(params={"download_archive": "/tmp/archive.txt"})
ytdlp.archive = Mock()
ytdlp._make_archive_id = Mock(return_value=None)
ytdlp.record_download_archive({"id": "test123"})
# Should not add anything
ytdlp.archive.add.assert_not_called()

View file

@ -9,7 +9,7 @@
<span class="icon"><i class="fa-solid fa-link" /></span>
<span class="has-tooltip" v-tooltip="'Use Shift+Enter to switch to multiline input mode.'">
URLs separated by newlines or <span class="is-bold is-lowercase">{{ getSeparatorsName(separator)
}}</span>
}}</span>
</span>
</label>
<div class="field is-grouped">
@ -33,11 +33,14 @@
</div>
<div class="column is-4-tablet is-12-mobile">
<label class="label" for="preset">
<span class="icon"><i class="fa-solid fa-sliders" /></span>
<span>Preset</span>
</label>
<div class="field has-addons">
<div class="control" @click="show_description = !show_description">
<label class="button is-static">
<span class="icon"><i class="fa-solid fa-sliders" /></span>
<span>Preset</span>
</label>
</div>
<div class="control is-expanded">
@ -62,22 +65,25 @@
</div>
<div class="column is-6-tablet is-12-mobile">
<div class="field has-addons" v-tooltip="'Folder relative to ' + config.app.download_path">
<div class="control">
<label class="label" for="folder">
<span class="icon"><i class="fa-solid fa-save" /></span>
<span>Download path</span>
</label>
<div class="field has-addons">
<div class="control" v-tooltip="`Download path relative to: ${config.app.download_path}`">
<label class="button is-static">
<span class="icon"><i class="fa-solid fa-folder" /></span>
<span>Save in</span>
<span>{{ shortPath(config.app.download_path) }}</span>
</label>
</div>
<div class="control is-expanded">
<input type="text" class="input is-fullwidth" id="path" v-model="form.folder"
<input type="text" class="input is-fullwidth" id="folder" v-model="form.folder"
:placeholder="getDefault('folder', '/')" :disabled="!socket.isConnected || addInProgress"
list="folders">
</div>
</div>
</div>
<div class="column">
<div class="column is-align-content-end">
<button type="button" class="button is-info" @click="showAdvanced = !showAdvanced"
:disabled="!socket.isConnected">
<span class="icon"><i class="fa-solid fa-cog" /></span>
@ -161,7 +167,7 @@
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>Use the <NuxtLink target="_blank"
to="https://github.com/yt-dlp/yt-dlp/wiki/FAQ#how-do-i-pass-cookies-to-yt-dlp">
Recommended addon</NuxtLink> by yt-dlp to export cookies. <span class="has-text-danger">The
Recommended addon</NuxtLink> to export cookies. <span class="has-text-danger">The
cookies MUST be in Netscape HTTP Cookie format.</span>
</span>
</span>
@ -401,12 +407,6 @@ const splitUrls = (urlString: string): Array<string> => {
}
const addDownload = async () => {
if (' ' === form.value?.folder) {
toast.warning('The download folder contain only spaces. Resetting to default.')
form.value.folder = ''
await nextTick()
}
if (form.value.folder) {
form.value.folder = form.value.folder.trim()
}
@ -578,14 +578,9 @@ onMounted(async () => {
form.value.preset = config.app.default_preset
}
if (' ' === form.value?.folder) {
toast.warning('The download folder contain only spaces. Resetting to default.')
form.value.folder = ''
await nextTick()
}
if (form.value.folder) {
if (form.value?.folder) {
form.value.folder = form.value.folder.trim()
await nextTick()
}
if (config.isLoaded() && form.value?.preset && !config.presets.some(p => p.name === form.value.preset)) {
@ -620,6 +615,11 @@ const runCliCommand = async (): Promise<void> => {
return
}
if (form.value?.folder) {
form.value.folder = form.value.folder.trim()
await nextTick()
}
const { status } = await dialog.confirmDialog({
title: 'Run CLI Command',
message: `This will generate a yt-dlp command and run it in the console. Continue?`,
@ -697,11 +697,13 @@ const testDownloadOptions = async (): Promise<void> => {
return
}
let form_cli = (form.value?.cli || '').trim()
if (' ' === form.value?.folder) {
form.value.folder = ''
if (form.value?.folder) {
form.value.folder = form.value.folder.trim()
await nextTick()
}
let form_cli = (form.value?.cli || '').trim()
if (dlFields.value && Object.keys(dlFields.value).length > 0) {
const joined = []
for (const [key, value] of Object.entries(dlFields.value)) {

View file

@ -1,7 +1,7 @@
<template>
<main class="columns mt-2 is-multiline">
<div class="column is-12">
<form autocomplete="off" id="presetForm" @submit.prevent="checkInfo()">
<form autocomplete="off" id="presetForm" @submit.prevent="checkInfo">
<div class="card">
<div class="card-header">
<div class="card-header-title is-text-overflow is-block">
@ -104,28 +104,32 @@
</div>
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="folder">
<span class="icon"><i class="fa-solid fa-folder" /></span>
Default Download path
</label>
<div class="control">
<label class="label is-inline" for="folder">
<span class="icon"><i class="fa-solid fa-save" /></span>
download path
</label>
<div class="field has-addons">
<div class="control" v-tooltip="`Full Path: ${config.app.download_path}`">
<span class="button is-static">
<span>{{ shortPath(config.app.download_path) }}</span>
</span>
</div>
<div class="control is-expanded">
<input type="text" class="input" id="folder" placeholder="Leave empty to use default download path"
v-model="form.folder" :disabled="addInProgress" list="folders">
</div>
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>Use this folder if non is given with URL. Leave empty to use default download path. Default
download path <code>{{ config.app.download_path }}</code>.</span>
</span>
</div>
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>Use this defined path if non are given with the URL.</span>
</span>
</div>
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="output_template">
<span class="icon"><i class="fa-solid fa-file" /></span>
Default Output template
Output template
</label>
<div class="control">
<input type="text" class="input" id="output_template" :disabled="addInProgress"
@ -133,10 +137,7 @@
</div>
<span class="help is-bold">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span>Use this output template if non are given with URL. if not set, it will defaults to
<code>{{ config.app.output_template }}</code>. For more information visit <NuxtLink
href="https://github.com/yt-dlp/yt-dlp#output-template" target="_blank">this page</NuxtLink>.
</span>
<span>Use this output template if non are given with URL.</span>
</span>
</div>
</div>
@ -277,6 +278,11 @@ const checkInfo = async (): Promise<void> => {
}
}
if (form.folder) {
form.folder = form.folder.trim()
await nextTick()
}
if (form.cli && '' !== form.cli) {
const options = await convertOptions(form.cli)
if (null === options) {

View file

@ -191,8 +191,8 @@
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span class="is-bold">
The CRON timer expression to use for this task. If not set, the task will be disabled. For more
information on CRON expressions, see <NuxtLink to="https://crontab.guru/" target="_blank">
The CRON timer expression to use for this task. If not set, the task runner will be disabled. For
more information on CRON expressions, see <NuxtLink to="https://crontab.guru/" target="_blank">
crontab.guru</NuxtLink>.
</span>
</span>
@ -200,21 +200,27 @@
</div>
<div class="column is-6-tablet is-12-mobile">
<div class="field">
<label class="label is-inline" for="folder">
<span class="icon"><i class="fa-solid fa-folder" /></span>
Save in
</label>
<div class="control">
<label class="label is-inline" for="folder">
<span class="icon"><i class="fa-solid fa-save" /></span>
Download path
</label>
<div class="field has-addons">
<div class="control" v-tooltip="`Full Path: ${config.app.download_path}`">
<span class="button is-static">
<span>{{ shortPath(config.app.download_path) }}</span>
</span>
</div>
<div class="control is-expanded">
<input type="text" class="input" id="folder" :placeholder="getDefault('folder', '/')"
v-model="form.folder" :disabled="addInProgress" list="folders">
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span class="is-bold">All folders are sub-folders of
<code>{{ config.app.download_path }}</code>.</span>
</span>
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span class="is-bold">
Path relative to the download path, leave empty to use preset or default download path.
</span>
</span>
</div>
<div class="column is-6-tablet is-12-mobile">
@ -230,7 +236,9 @@
</div>
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span class="is-bold">The template to use for the output file name.</span>
<span class="is-bold">
The template to use for the output file name. Leave empty to use preset or default template.
</span>
</span>
</div>
</div>
@ -251,7 +259,8 @@
<span class="help">
<span class="icon"><i class="fa-solid fa-info" /></span>
<span class="is-bold">Some URLs have special handlers to monitor for new content. Like YouTube
channels/playlists.</span>
channels/playlists. <span class="has-text-danger">Handlers run regardless of task timer.</span>
</span>
</span>
</div>
</div>
@ -324,6 +333,10 @@
<Message class="is-info" :newStyle="true">
<span>
<ul>
<li><strong>Tasks:</strong> requires <code>--download-archive</code> in
<code>Command options for yt-dlp</code> can be set via presets or manually. Default presets already
include this option.
</li>
<li><strong>YouTube RSS:</strong> Use <code>channel_id</code> or <code>playlist_id</code> URLs. Other link
types (custom names, handles, user profiles) are not supported.
</li>
@ -334,9 +347,6 @@
<li><strong>RSS Monitoring Basics:</strong> Runs hourly independently. Timer controls scheduled downloads to
yt-dlp. Disable <code>Enable Handler</code> to disable RSS monitoring.
</li>
<li><strong>Archive Requirement:</strong> RSS monitoring requires <code>--download-archive</code> in
<code>Command options for yt-dlp</code> (set in task or preset).
</li>
</ul>
</span>
</Message>
@ -420,6 +430,11 @@ const checkInfo = async (): Promise<void> => {
}
}
if (form.folder) {
form.folder = form.folder.trim()
await nextTick()
}
if (form.timer) {
try {
CronExpressionParser.parse(form.timer)

View file

@ -129,7 +129,7 @@
<p :class="{ 'is-text-overflow': !isExpanded(item.id, 'folder'), 'is-clickable': true }"
v-if="item.folder" @click="toggleExpand(item.id, 'folder')"
:title="!isExpanded(item.id, 'folder') ? 'Click to expand' : 'Click to collapse'">
<span class="icon"><i class="fa-solid fa-folder" /></span>
<span class="icon"><i class="fa-solid fa-save" /></span>
<span>{{ calcPath(item.folder) }}</span>
</p>
<p :class="{ 'is-text-overflow': !isExpanded(item.id, 'template'), 'is-clickable': true }"

View file

@ -332,7 +332,7 @@
<span v-else class="has-text-danger">No timer or handler</span>
</p>
<p class="is-text-overflow" v-if="item.folder">
<span class="icon"><i class="fa-solid fa-folder" /></span>
<span class="icon"><i class="fa-solid fa-save" /></span>
<span>{{ calcPath(item.folder) }}</span>
</p>
<p class="is-text-overflow" v-if="item.template">

View file

@ -703,11 +703,26 @@ const stripPath = (base_path: string, real_path: string): string => {
return real_path.replace(base_path, '').replace(/^\//, '')
}
const shortPath = (path: string, prefix: string = '...'): string => {
if (typeof path !== 'string') {
return path;
}
const hasTrailingSlash = /\/$/.test(path);
const clean = path.replace(/\/+$/, '');
const parts = clean.split('/').filter(Boolean);
if (parts.length <= 1) {
return path;
}
return `${prefix}/${parts.at(-1)}${hasTrailingSlash ? '/' : ''}`;
}
export {
separators, convertCliOptions, getSeparatorsName, iTrim, eTrim, sTrim, ucFirst,
getValue, ag, ag_set, awaitElement, r, copyText, dEvent, makePagination, encodePath,
request, removeANSIColors, dec2hex, makeId, basename, dirname, getQueryParams,
makeDownload, formatBytes, has_data, toggleClass, cleanObject, uri, formatTime,
sleep, awaiter, encode, decode, disableOpacity, enableOpacity, stripPath
sleep, awaiter, encode, decode, disableOpacity, enableOpacity, stripPath, shortPath
}

View file

@ -34,7 +34,7 @@
"moment": "^2.30.1",
"nuxt": "^4.2.2",
"pinia": "^3.0.4",
"socket.io-client": "^4.8.2",
"socket.io-client": "^4.8.3",
"vue": "^3.5.26",
"vue-router": "^4.6.4",
"vue-toastification": "2.0.0-rc.5"

View file

@ -57,8 +57,8 @@ importers:
specifier: ^3.0.4
version: 3.0.4(typescript@5.9.3)(vue@3.5.26(typescript@5.9.3))
socket.io-client:
specifier: ^4.8.2
version: 4.8.2
specifier: ^4.8.3
version: 4.8.3
vue:
specifier: ^3.5.26
version: 3.5.26(typescript@5.9.3)
@ -1267,8 +1267,8 @@ packages:
'@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==}
'@rolldown/pluginutils@1.0.0-beta.57':
resolution: {integrity: sha512-aQNelgx14tGA+n2tNSa9x6/jeoCL9fkDeCei7nOKnHx0fEFRRMu5ReiITo+zZD5TzWDGGRjbSYCs93IfRIyTuQ==}
'@rollup/plugin-alias@5.1.1':
resolution: {integrity: sha512-PR9zDb+rOzkRb2VD+EuKB7UC41vU5DIwZ5qqCpk0KJudcWAyi8rvYOhS7+L5aZCspw1stTViLgN5v6FF1p5cgQ==}
@ -1577,8 +1577,8 @@ packages:
resolution: {integrity: sha512-IrDKrw7pCRUR94zeuCSUWQ+w8JEf5ZX5jl/e6AHGSLi1/zIr0lgutfn/7JpfCey+urpgQEdrZVYzCaVVKiTwhQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@unhead/vue@2.0.19':
resolution: {integrity: sha512-7BYjHfOaoZ9+ARJkT10Q2TjnTUqDXmMpfakIAsD/hXiuff1oqWg1xeXT5+MomhNcC15HbiABpbbBmITLSHxdKg==}
'@unhead/vue@2.1.1':
resolution: {integrity: sha512-WYa8ORhfv7lWDSoNpkMKhbW1Dbsux/3HqMcVkZS3xZ2/c/VrcChLj+IMadpCd1WNR0srITfRJhBYZ1i9hON5Qw==}
peerDependencies:
vue: '>=3.5.18'
@ -1690,11 +1690,11 @@ packages:
engines: {node: '>=18'}
hasBin: true
'@vitejs/plugin-vue-jsx@5.1.2':
resolution: {integrity: sha512-3a2BOryRjG/Iih87x87YXz5c8nw27eSlHytvSKYfp8ZIsp5+FgFQoKeA7k2PnqWpjJrv6AoVTMnvmuKUXb771A==}
'@vitejs/plugin-vue-jsx@5.1.3':
resolution: {integrity: sha512-I6Zr8cYVr5WHMW5gNOP09DNqW9rgO8RX73Wa6Czgq/0ndpTfJM4vfDChfOT1+3KtdrNqilNBtNlFwVeB02ZzGw==}
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.0.0
'@vitejs/plugin-vue@6.0.3':
@ -1877,8 +1877,8 @@ packages:
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
alien-signals@3.1.1:
resolution: {integrity: sha512-ogkIWbVrLwKtHY6oOAXaYkAxP+cTH7V5FZ5+Tm4NZFd8VDZ6uNMDrfzqctTZ42eTMCSR3ne3otpcxmqSnFfPYA==}
alien-signals@3.1.2:
resolution: {integrity: sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==}
ansi-regex@5.0.1:
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
@ -3831,8 +3831,8 @@ packages:
serialize-javascript@6.0.2:
resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==}
seroval@1.4.1:
resolution: {integrity: sha512-9GOc+8T6LN4aByLN75uRvMbrwY5RDBW6lSlknsY4LEa9ZmWcxKcRe1G/Q3HZXjltxMHTrStnvrwAICxZrhldtg==}
seroval@1.4.2:
resolution: {integrity: sha512-N3HEHRCZYn3cQbsC4B5ldj9j+tHdf4JZoYPlcI4rRYu0Xy4qN8MQf1Z08EibzB0WpgRG5BGK08FTrmM66eSzKQ==}
engines: {node: '>=10'}
serve-placeholder@2.0.2:
@ -3881,8 +3881,8 @@ packages:
smob@1.5.0:
resolution: {integrity: sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==}
socket.io-client@4.8.2:
resolution: {integrity: sha512-4MY14EMsyEPFA6lM01XIYepRdV8P6dUir2hxAlAysOYcbNAy5QNHYgIHOcQ1KYM7wTcKnKEW/ZRoIxRinWRXvA==}
socket.io-client@4.8.3:
resolution: {integrity: sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==}
engines: {node: '>=10.0.0'}
socket.io-parser@4.2.5:
@ -4130,8 +4130,8 @@ packages:
unenv@2.0.0-rc.24:
resolution: {integrity: sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw==}
unhead@2.0.19:
resolution: {integrity: sha512-gEEjkV11Aj+rBnY6wnRfsFtF2RxKOLaPN4i+Gx3UhBxnszvV6ApSNZbGk7WKyy/lErQ6ekPN63qdFL7sa1leow==}
unhead@2.1.1:
resolution: {integrity: sha512-NOt8n2KybAOxSLfNXegAVai4SGU8bPKqWnqCzNAvnRH2i8mW+0bbFjN/L75LBgCSTiOjJSpANe5w2V34Grr7Cw==}
unicorn-magic@0.3.0:
resolution: {integrity: sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==}
@ -4456,6 +4456,7 @@ packages:
whatwg-encoding@3.1.1:
resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==}
engines: {node: '>=18'}
deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation
whatwg-mimetype@4.0.0:
resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==}
@ -5421,7 +5422,7 @@ snapshots:
dependencies:
'@nuxt/devalue': 2.0.2
'@nuxt/kit': 4.2.2(magicast@0.5.1)
'@unhead/vue': 2.0.19(vue@3.5.26(typescript@5.9.3))
'@unhead/vue': 2.1.1(vue@3.5.26(typescript@5.9.3))
'@vue/shared': 3.5.26
consola: 3.4.2
defu: 6.1.4
@ -5511,7 +5512,7 @@ snapshots:
'@nuxt/kit': 4.2.2(magicast@0.5.1)
'@rollup/plugin-replace': 6.0.3(rollup@4.54.0)
'@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.26(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.26(typescript@5.9.3))
'@vitejs/plugin-vue-jsx': 5.1.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.26(typescript@5.9.3))
autoprefixer: 10.4.23(postcss@8.5.6)
consola: 3.4.2
cssnano: 7.1.2(postcss@8.5.6)
@ -5531,7 +5532,7 @@ snapshots:
pkg-types: 2.3.0
postcss: 8.5.6
rollup-plugin-visualizer: 6.0.5(rollup@4.54.0)
seroval: 1.4.1
seroval: 1.4.2
std-env: 3.10.0
ufo: 1.6.1
unenv: 2.0.0-rc.24
@ -5799,7 +5800,7 @@ snapshots:
'@rolldown/pluginutils@1.0.0-beta.53': {}
'@rolldown/pluginutils@1.0.0-beta.9-commit.d91dfb5': {}
'@rolldown/pluginutils@1.0.0-beta.57': {}
'@rollup/plugin-alias@5.1.1(rollup@4.54.0)':
optionalDependencies:
@ -6072,10 +6073,10 @@ snapshots:
'@typescript-eslint/types': 8.50.1
eslint-visitor-keys: 4.2.1
'@unhead/vue@2.0.19(vue@3.5.26(typescript@5.9.3))':
'@unhead/vue@2.1.1(vue@3.5.26(typescript@5.9.3))':
dependencies:
hookable: 5.5.3
unhead: 2.0.19
unhead: 2.1.1
vue: 3.5.26(typescript@5.9.3)
'@unrs/resolver-binding-android-arm-eabi@1.11.1':
@ -6156,12 +6157,12 @@ snapshots:
- rollup
- supports-color
'@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.26(typescript@5.9.3))':
'@vitejs/plugin-vue-jsx@5.1.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.26(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.9-commit.d91dfb5
'@rolldown/pluginutils': 1.0.0-beta.57
'@vue/babel-plugin-jsx': 2.0.1(@babel/core@7.28.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.26(typescript@5.9.3)
@ -6345,7 +6346,7 @@ snapshots:
'@volar/language-core': 2.4.27
'@vue/compiler-dom': 3.5.26
'@vue/shared': 3.5.26
alien-signals: 3.1.1
alien-signals: 3.1.2
muggle-string: 0.4.1
path-browserify: 1.0.1
picomatch: 4.0.3
@ -6427,7 +6428,7 @@ snapshots:
json-schema-traverse: 0.4.1
uri-js: 4.4.1
alien-signals@3.1.1: {}
alien-signals@3.1.2: {}
ansi-regex@5.0.1: {}
@ -7947,7 +7948,7 @@ snapshots:
'@nuxt/schema': 4.2.2
'@nuxt/telemetry': 2.6.6(magicast@0.5.1)
'@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.26)(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.54.0)(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.2.1(typescript@5.9.3))(yaml@2.8.2))(optionator@0.9.4)(rollup@4.54.0)(terser@5.44.1)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2)
'@unhead/vue': 2.0.19(vue@3.5.26(typescript@5.9.3))
'@unhead/vue': 2.1.1(vue@3.5.26(typescript@5.9.3))
'@vue/shared': 3.5.26
c12: 3.3.3(magicast@0.5.1)
chokidar: 5.0.0
@ -8628,7 +8629,7 @@ snapshots:
dependencies:
randombytes: 2.1.0
seroval@1.4.1: {}
seroval@1.4.2: {}
serve-placeholder@2.0.2:
dependencies:
@ -8677,7 +8678,7 @@ snapshots:
smob@1.5.0: {}
socket.io-client@4.8.2:
socket.io-client@4.8.3:
dependencies:
'@socket.io/component-emitter': 3.1.2
debug: 4.4.3
@ -8928,7 +8929,7 @@ snapshots:
dependencies:
pathe: 2.0.3
unhead@2.0.19:
unhead@2.1.1:
dependencies:
hookable: 5.5.3