From d211f24e00f2cdd74dc4a794845e706219b023c2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:27:55 +0000 Subject: [PATCH 001/107] Initial plan From 1f4c4df847cd40066155cffa18c6306a5adc9ecd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:37:35 +0000 Subject: [PATCH 002/107] Implement DEFAULT_DOWNLOAD_FOLDER and CLEAR_COMPLETED_AFTER features (#875, #869) Co-authored-by: alexta69 <7450369+alexta69@users.noreply.github.com> --- README.md | 2 ++ app/main.py | 2 ++ app/ytdl.py | 14 ++++++++++++++ ui/src/app/app.ts | 4 ++++ 4 files changed, 22 insertions(+) diff --git a/README.md b/README.md index a913a0f..48cc61c 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,7 @@ Certain values can be set via environment variables, using the `-e` parameter on * __MAX_CONCURRENT_DOWNLOADS__: Maximum number of simultaneous downloads allowed. For example, if set to `5`, then at most five downloads will run concurrently, and any additional downloads will wait until one of the active downloads completes. Defaults to `3`. * __DELETE_FILE_ON_TRASHCAN__: if `true`, downloaded files are deleted on the server, when they are trashed from the "Completed" section of the UI. Defaults to `false`. * __DEFAULT_OPTION_PLAYLIST_ITEM_LIMIT__: Maximum number of playlist items that can be downloaded. Defaults to `0` (no limit). +* __CLEAR_COMPLETED_AFTER__: Number of seconds after which completed (and failed) downloads are automatically removed from the "Completed" list. Defaults to `0` (disabled). ### πŸ“ Storage & Directories @@ -43,6 +44,7 @@ Certain values can be set via environment variables, using the `-e` parameter on * __AUDIO_DOWNLOAD_DIR__: Path to where audio-only downloads will be saved, if you wish to separate them from the video downloads. Defaults to the value of `DOWNLOAD_DIR`. * __CUSTOM_DIRS__: Whether to enable downloading videos into custom directories within the __DOWNLOAD_DIR__ (or __AUDIO_DOWNLOAD_DIR__). When enabled, a dropdown appears next to the Add button to specify the download directory. Defaults to `true`. * __CREATE_CUSTOM_DIRS__: Whether to support automatically creating directories within the __DOWNLOAD_DIR__ (or __AUDIO_DOWNLOAD_DIR__) if they do not exist. When enabled, the download directory selector supports free-text input, and the specified directory will be created recursively. Defaults to `true`. +* __DEFAULT_DOWNLOAD_FOLDER__: Default subdirectory within __DOWNLOAD_DIR__ (or __AUDIO_DOWNLOAD_DIR__) to pre-select in the download folder field. Useful when most downloads should go to a specific subfolder. Can be overridden per-download in the UI. Defaults to empty (uses the base download directory). * __CUSTOM_DIRS_EXCLUDE_REGEX__: Regular expression to exclude some custom directories from the dropdown. Empty regex disables exclusion. Defaults to `(^|/)[.@].*$`, which means directories starting with `.` or `@`. * __DOWNLOAD_DIRS_INDEXABLE__: If `true`, the download directories (__DOWNLOAD_DIR__ and __AUDIO_DOWNLOAD_DIR__) are indexable on the web server. Defaults to `false`. * __STATE_DIR__: Path to where the queue persistence files will be saved. Defaults to `/downloads/.metube` in the Docker image, and `.` otherwise. diff --git a/app/main.py b/app/main.py index 1c4bd66..df8b105 100644 --- a/app/main.py +++ b/app/main.py @@ -60,6 +60,8 @@ class Config: 'OUTPUT_TEMPLATE_PLAYLIST': '%(playlist_title)s/%(title)s.%(ext)s', 'OUTPUT_TEMPLATE_CHANNEL': '%(channel)s/%(title)s.%(ext)s', 'DEFAULT_OPTION_PLAYLIST_ITEM_LIMIT' : '0', + 'DEFAULT_DOWNLOAD_FOLDER': '', + 'CLEAR_COMPLETED_AFTER': '0', 'YTDL_OPTIONS': '{}', 'YTDL_OPTIONS_FILE': '', 'ROBOTS_TXT': '', diff --git a/app/ytdl.py b/app/ytdl.py index bc84b74..f64f669 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -573,6 +573,20 @@ class DownloadQueue: else: self.done.put(download) asyncio.create_task(self.notifier.completed(download.info)) + try: + clear_after = int(self.config.CLEAR_COMPLETED_AFTER) + except ValueError: + log.error(f'CLEAR_COMPLETED_AFTER is set to an invalid value "{self.config.CLEAR_COMPLETED_AFTER}", expected an integer number of seconds') + clear_after = 0 + if clear_after > 0: + task = asyncio.create_task(self.__auto_clear_after_delay(download.info.url, clear_after)) + task.add_done_callback(lambda t: log.error(f'Auto-clear task failed: {t.exception()}') if not t.cancelled() and t.exception() else None) + + async def __auto_clear_after_delay(self, url, delay_seconds): + await asyncio.sleep(delay_seconds) + if self.done.exists(url): + log.debug(f'Auto-clearing completed download: {url}') + await self.clear([url]) def __extract_info(self, url): debug_logging = logging.getLogger().isEnabledFor(logging.DEBUG) diff --git a/ui/src/app/app.ts b/ui/src/app/app.ts index 15f7ce3..186ed91 100644 --- a/ui/src/app/app.ts +++ b/ui/src/app/app.ts @@ -315,6 +315,10 @@ export class App implements AfterViewInit, OnInit { if (!this.chapterTemplate) { this.chapterTemplate = config['OUTPUT_TEMPLATE_CHAPTER']; } + // Set default download folder from backend config if not already set + if (!this.folder && config['DEFAULT_DOWNLOAD_FOLDER']) { + this.folder = config['DEFAULT_DOWNLOAD_FOLDER']; + } } }); } From 6de4a56f28928802eb625f4bedb0887e6caf76ea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Mar 2026 06:37:09 +0000 Subject: [PATCH 003/107] Remove DEFAULT_DOWNLOAD_FOLDER feature Co-authored-by: alexta69 <7450369+alexta69@users.noreply.github.com> --- README.md | 1 - app/main.py | 1 - ui/src/app/app.ts | 4 ---- 3 files changed, 6 deletions(-) diff --git a/README.md b/README.md index 48cc61c..a5a322d 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,6 @@ Certain values can be set via environment variables, using the `-e` parameter on * __AUDIO_DOWNLOAD_DIR__: Path to where audio-only downloads will be saved, if you wish to separate them from the video downloads. Defaults to the value of `DOWNLOAD_DIR`. * __CUSTOM_DIRS__: Whether to enable downloading videos into custom directories within the __DOWNLOAD_DIR__ (or __AUDIO_DOWNLOAD_DIR__). When enabled, a dropdown appears next to the Add button to specify the download directory. Defaults to `true`. * __CREATE_CUSTOM_DIRS__: Whether to support automatically creating directories within the __DOWNLOAD_DIR__ (or __AUDIO_DOWNLOAD_DIR__) if they do not exist. When enabled, the download directory selector supports free-text input, and the specified directory will be created recursively. Defaults to `true`. -* __DEFAULT_DOWNLOAD_FOLDER__: Default subdirectory within __DOWNLOAD_DIR__ (or __AUDIO_DOWNLOAD_DIR__) to pre-select in the download folder field. Useful when most downloads should go to a specific subfolder. Can be overridden per-download in the UI. Defaults to empty (uses the base download directory). * __CUSTOM_DIRS_EXCLUDE_REGEX__: Regular expression to exclude some custom directories from the dropdown. Empty regex disables exclusion. Defaults to `(^|/)[.@].*$`, which means directories starting with `.` or `@`. * __DOWNLOAD_DIRS_INDEXABLE__: If `true`, the download directories (__DOWNLOAD_DIR__ and __AUDIO_DOWNLOAD_DIR__) are indexable on the web server. Defaults to `false`. * __STATE_DIR__: Path to where the queue persistence files will be saved. Defaults to `/downloads/.metube` in the Docker image, and `.` otherwise. diff --git a/app/main.py b/app/main.py index df8b105..54bc952 100644 --- a/app/main.py +++ b/app/main.py @@ -60,7 +60,6 @@ class Config: 'OUTPUT_TEMPLATE_PLAYLIST': '%(playlist_title)s/%(title)s.%(ext)s', 'OUTPUT_TEMPLATE_CHANNEL': '%(channel)s/%(title)s.%(ext)s', 'DEFAULT_OPTION_PLAYLIST_ITEM_LIMIT' : '0', - 'DEFAULT_DOWNLOAD_FOLDER': '', 'CLEAR_COMPLETED_AFTER': '0', 'YTDL_OPTIONS': '{}', 'YTDL_OPTIONS_FILE': '', diff --git a/ui/src/app/app.ts b/ui/src/app/app.ts index 186ed91..15f7ce3 100644 --- a/ui/src/app/app.ts +++ b/ui/src/app/app.ts @@ -315,10 +315,6 @@ export class App implements AfterViewInit, OnInit { if (!this.chapterTemplate) { this.chapterTemplate = config['OUTPUT_TEMPLATE_CHAPTER']; } - // Set default download folder from backend config if not already set - if (!this.folder && config['DEFAULT_DOWNLOAD_FOLDER']) { - this.folder = config['DEFAULT_DOWNLOAD_FOLDER']; - } } }); } From 0d905c0b615adcba56381701f697a38eafc0e06d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Mar 2026 07:19:05 +0000 Subject: [PATCH 004/107] Fix issues #898, #542, #561, #504 Co-authored-by: alexta69 <7450369+alexta69@users.noreply.github.com> --- README.md | 4 ++-- app/main.py | 31 +++++++++++++++++++++++++++---- app/ytdl.py | 24 ++++++++++++++++++++++-- 3 files changed, 51 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a5a322d..77d9ff1 100644 --- a/README.md +++ b/README.md @@ -56,8 +56,8 @@ Certain values can be set via environment variables, using the `-e` parameter on * __OUTPUT_TEMPLATE__: The template for the filenames of the downloaded videos, formatted according to [this spec](https://github.com/yt-dlp/yt-dlp/blob/master/README.md#output-template). Defaults to `%(title)s.%(ext)s`. * __OUTPUT_TEMPLATE_CHAPTER__: The template for the filenames of the downloaded videos when split into chapters via postprocessors. Defaults to `%(title)s - %(section_number)s %(section_title)s.%(ext)s`. -* __OUTPUT_TEMPLATE_PLAYLIST__: The template for the filenames of the downloaded videos when downloaded as a playlist. Defaults to `%(playlist_title)s/%(title)s.%(ext)s`. When empty, then `OUTPUT_TEMPLATE` is used. -* __OUTPUT_TEMPLATE_CHANNEL__: The template for the filenames of the downloaded videos when downloaded as a channel. Defaults to `%(channel)s/%(title)s.%(ext)s`. When empty, then `OUTPUT_TEMPLATE` is used. +* __OUTPUT_TEMPLATE_PLAYLIST__: The template for the filenames of the downloaded videos when downloaded as a playlist. Defaults to empty (uses `OUTPUT_TEMPLATE`). Set to e.g. `%(playlist_title)s/%(title)s.%(ext)s` to group each playlist into its own subdirectory. +* __OUTPUT_TEMPLATE_CHANNEL__: The template for the filenames of the downloaded videos when downloaded as a channel. Defaults to empty (uses `OUTPUT_TEMPLATE`). Set to e.g. `%(channel)s/%(title)s.%(ext)s` to group each channel into its own subdirectory. * __YTDL_OPTIONS__: Additional options to pass to yt-dlp in JSON format. [See available options here](https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/YoutubeDL.py#L222). They roughly correspond to command-line options, though some do not have exact equivalents here. For example, `--recode-video` has to be specified via `postprocessors`. Also note that dashes are replaced with underscores. You may find [this script](https://github.com/yt-dlp/yt-dlp/blob/master/devscripts/cli_to_api.py) helpful for converting from command-line options to `YTDL_OPTIONS`. * __YTDL_OPTIONS_FILE__: A path to a JSON file that will be loaded and used for populating `YTDL_OPTIONS` above. Please note that if both `YTDL_OPTIONS_FILE` and `YTDL_OPTIONS` are specified, the options in `YTDL_OPTIONS` take precedence. The file will be monitored for changes and reloaded automatically when changes are detected. diff --git a/app/main.py b/app/main.py index 54bc952..d2a1923 100644 --- a/app/main.py +++ b/app/main.py @@ -57,8 +57,8 @@ class Config: 'PUBLIC_HOST_AUDIO_URL': 'audio_download/', 'OUTPUT_TEMPLATE': '%(title)s.%(ext)s', 'OUTPUT_TEMPLATE_CHAPTER': '%(title)s - %(section_number)02d - %(section_title)s.%(ext)s', - 'OUTPUT_TEMPLATE_PLAYLIST': '%(playlist_title)s/%(title)s.%(ext)s', - 'OUTPUT_TEMPLATE_CHANNEL': '%(channel)s/%(title)s.%(ext)s', + 'OUTPUT_TEMPLATE_PLAYLIST': '', + 'OUTPUT_TEMPLATE_CHANNEL': '', 'DEFAULT_OPTION_PLAYLIST_ITEM_LIMIT' : '0', 'CLEAR_COMPLETED_AFTER': '0', 'YTDL_OPTIONS': '{}', @@ -115,6 +115,25 @@ class Config: def _apply_runtime_overrides(self): self.YTDL_OPTIONS.update(self._runtime_overrides) + # Keys sent to the browser. Sensitive or server-only keys (YTDL_OPTIONS, + # paths, TLS config, etc.) are intentionally excluded. + _FRONTEND_KEYS = ( + 'CUSTOM_DIRS', + 'CREATE_CUSTOM_DIRS', + 'OUTPUT_TEMPLATE_CHAPTER', + 'PUBLIC_HOST_URL', + 'PUBLIC_HOST_AUDIO_URL', + 'DEFAULT_OPTION_PLAYLIST_ITEM_LIMIT', + ) + + def frontend_safe(self) -> dict: + """Return only the config keys that are safe to expose to browser clients. + + Sensitive or server-only keys (YTDL_OPTIONS, file-system paths, TLS + settings, etc.) are intentionally excluded. + """ + return {k: getattr(self, k) for k in self._FRONTEND_KEYS} + def load_ytdl_options(self) -> tuple[bool, str]: try: self.YTDL_OPTIONS = json.loads(os.environ.get('YTDL_OPTIONS', '{}')) @@ -420,7 +439,7 @@ async def history(request): async def connect(sid, environ): log.info(f"Client connected: {sid}") await sio.emit('all', serializer.encode(dqueue.get()), to=sid) - await sio.emit('configuration', serializer.encode(config), to=sid) + await sio.emit('configuration', serializer.encode(config.frontend_safe()), to=sid) if config.CUSTOM_DIRS: await sio.emit('custom_dirs', serializer.encode(get_custom_dirs()), to=sid) if config.YTDL_OPTIONS_FILE: @@ -448,8 +467,12 @@ def get_custom_dirs(): else: return re.search(config.CUSTOM_DIRS_EXCLUDE_REGEX, d) is None - # Recursively lists all subdirectories of DOWNLOAD_DIR + # Recursively lists all subdirectories of DOWNLOAD_DIR. + # Always include '' (the base directory itself) even when the + # directory is empty or does not yet exist. dirs = list(filter(include_dir, map(convert, path.glob('**/')))) + if '' not in dirs: + dirs.insert(0, '') return dirs diff --git a/app/ytdl.py b/app/ytdl.py index f64f669..f73eb36 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -29,6 +29,26 @@ def _compile_outtmpl_pattern(field: str) -> re.Pattern: return re.compile(STR_FORMAT_RE_TMPL.format(re.escape(field), conversion_types)) +# Characters that are invalid in Windows/NTFS path components. These are pre- +# sanitised when substituting playlist/channel titles into output templates so +# that downloads do not fail on NTFS-mounted volumes or Windows Docker hosts. +_WINDOWS_INVALID_PATH_CHARS = re.compile(r'[\\:*?"<>|]') + + +def _sanitize_path_component(value: Any) -> Any: + """Replace characters that are invalid in Windows path components with '_'. + + Non-string values (int, float, None, …) are passed through unchanged so + that ``_outtmpl_substitute_field`` can still coerce them with format specs + (e.g. ``%(playlist_index)02d``). Only string values are sanitised because + Windows-invalid characters are only a concern for human-readable strings + (titles, channel names, etc.) that may end up as directory names. + """ + if not isinstance(value, str): + return value + return _WINDOWS_INVALID_PATH_CHARS.sub('_', value) + + def _outtmpl_substitute_field(template: str, field: str, value: Any) -> str: """Substitute a single field in an output template, applying any format specifiers to the value.""" pattern = _compile_outtmpl_pattern(field) @@ -631,13 +651,13 @@ class DownloadQueue: output = self.config.OUTPUT_TEMPLATE_PLAYLIST for property, value in entry.items(): if property.startswith("playlist"): - output = _outtmpl_substitute_field(output, property, value) + output = _outtmpl_substitute_field(output, property, _sanitize_path_component(value)) if entry is not None and entry.get('channel_index') is not None: if len(self.config.OUTPUT_TEMPLATE_CHANNEL): output = self.config.OUTPUT_TEMPLATE_CHANNEL for property, value in entry.items(): if property.startswith("channel"): - output = _outtmpl_substitute_field(output, property, value) + output = _outtmpl_substitute_field(output, property, _sanitize_path_component(value)) ytdl_options = dict(self.config.YTDL_OPTIONS) playlist_item_limit = getattr(dl, 'playlist_item_limit', 0) if playlist_item_limit > 0: From 2736425e197fffc183374ffebd71988c6f41ec00 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 7 Mar 2026 14:00:29 +0000 Subject: [PATCH 005/107] Revert #504 default change: restore original playlist/channel output templates Co-authored-by: alexta69 <7450369+alexta69@users.noreply.github.com> --- README.md | 4 ++-- app/main.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 77d9ff1..16228af 100644 --- a/README.md +++ b/README.md @@ -56,8 +56,8 @@ Certain values can be set via environment variables, using the `-e` parameter on * __OUTPUT_TEMPLATE__: The template for the filenames of the downloaded videos, formatted according to [this spec](https://github.com/yt-dlp/yt-dlp/blob/master/README.md#output-template). Defaults to `%(title)s.%(ext)s`. * __OUTPUT_TEMPLATE_CHAPTER__: The template for the filenames of the downloaded videos when split into chapters via postprocessors. Defaults to `%(title)s - %(section_number)s %(section_title)s.%(ext)s`. -* __OUTPUT_TEMPLATE_PLAYLIST__: The template for the filenames of the downloaded videos when downloaded as a playlist. Defaults to empty (uses `OUTPUT_TEMPLATE`). Set to e.g. `%(playlist_title)s/%(title)s.%(ext)s` to group each playlist into its own subdirectory. -* __OUTPUT_TEMPLATE_CHANNEL__: The template for the filenames of the downloaded videos when downloaded as a channel. Defaults to empty (uses `OUTPUT_TEMPLATE`). Set to e.g. `%(channel)s/%(title)s.%(ext)s` to group each channel into its own subdirectory. +* __OUTPUT_TEMPLATE_PLAYLIST__: The template for the filenames of the downloaded videos when downloaded as a playlist. Defaults to `%(playlist_title)s/%(title)s.%(ext)s`. Set to empty to use `OUTPUT_TEMPLATE` instead. +* __OUTPUT_TEMPLATE_CHANNEL__: The template for the filenames of the downloaded videos when downloaded as a channel. Defaults to `%(channel)s/%(title)s.%(ext)s`. Set to empty to use `OUTPUT_TEMPLATE` instead. * __YTDL_OPTIONS__: Additional options to pass to yt-dlp in JSON format. [See available options here](https://github.com/yt-dlp/yt-dlp/blob/master/yt_dlp/YoutubeDL.py#L222). They roughly correspond to command-line options, though some do not have exact equivalents here. For example, `--recode-video` has to be specified via `postprocessors`. Also note that dashes are replaced with underscores. You may find [this script](https://github.com/yt-dlp/yt-dlp/blob/master/devscripts/cli_to_api.py) helpful for converting from command-line options to `YTDL_OPTIONS`. * __YTDL_OPTIONS_FILE__: A path to a JSON file that will be loaded and used for populating `YTDL_OPTIONS` above. Please note that if both `YTDL_OPTIONS_FILE` and `YTDL_OPTIONS` are specified, the options in `YTDL_OPTIONS` take precedence. The file will be monitored for changes and reloaded automatically when changes are detected. diff --git a/app/main.py b/app/main.py index d2a1923..74ac76a 100644 --- a/app/main.py +++ b/app/main.py @@ -57,8 +57,8 @@ class Config: 'PUBLIC_HOST_AUDIO_URL': 'audio_download/', 'OUTPUT_TEMPLATE': '%(title)s.%(ext)s', 'OUTPUT_TEMPLATE_CHAPTER': '%(title)s - %(section_number)02d - %(section_title)s.%(ext)s', - 'OUTPUT_TEMPLATE_PLAYLIST': '', - 'OUTPUT_TEMPLATE_CHANNEL': '', + 'OUTPUT_TEMPLATE_PLAYLIST': '%(playlist_title)s/%(title)s.%(ext)s', + 'OUTPUT_TEMPLATE_CHANNEL': '%(channel)s/%(title)s.%(ext)s', 'DEFAULT_OPTION_PLAYLIST_ITEM_LIMIT' : '0', 'CLEAR_COMPLETED_AFTER': '0', 'YTDL_OPTIONS': '{}', From 3bbe1e8424f258a3611c39409d4f8a5d65cd7ae2 Mon Sep 17 00:00:00 2001 From: CyCl0ne Date: Sun, 8 Mar 2026 14:56:16 +0100 Subject: [PATCH 006/107] Add "Downloaded" timestamp column to completed downloads list Display the completion time for each download in the done list. The backend already stores a nanosecond timestamp on DownloadInfo; this wires it up to the frontend using Angular's DatePipe. --- ui/src/app/app.html | 7 +++++++ ui/src/app/app.ts | 3 ++- ui/src/app/interfaces/download.ts | 1 + 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/ui/src/app/app.html b/ui/src/app/app.html index 6504932..202ee80 100644 --- a/ui/src/app/app.html +++ b/ui/src/app/app.html @@ -490,6 +490,7 @@ Video File Size + Downloaded @@ -556,6 +557,11 @@ {{ entry[1].size | fileSize }} } + + @if (entry[1].timestamp) { + {{ entry[1].timestamp / 1000000 | date:'yyyy-MM-dd HH:mm' }} + } +
@if (entry[1].status === 'error') { @@ -585,6 +591,7 @@ {{ chapterFile.size | fileSize }} } +
Date: Sun, 8 Mar 2026 16:12:41 +0000 Subject: [PATCH 007/107] Bump the github-actions group with 4 updates Bumps the github-actions group with 4 updates: [docker/setup-qemu-action](https://github.com/docker/setup-qemu-action), [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action), [docker/login-action](https://github.com/docker/login-action) and [docker/build-push-action](https://github.com/docker/build-push-action). Updates `docker/setup-qemu-action` from 3 to 4 - [Release notes](https://github.com/docker/setup-qemu-action/releases) - [Commits](https://github.com/docker/setup-qemu-action/compare/v3...v4) Updates `docker/setup-buildx-action` from 3 to 4 - [Release notes](https://github.com/docker/setup-buildx-action/releases) - [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4) Updates `docker/login-action` from 3 to 4 - [Release notes](https://github.com/docker/login-action/releases) - [Commits](https://github.com/docker/login-action/compare/v3...v4) Updates `docker/build-push-action` from 6 to 7 - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v6...v7) --- updated-dependencies: - dependency-name: docker/setup-qemu-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: docker/setup-buildx-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: docker/login-action dependency-version: '4' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions - dependency-name: docker/build-push-action dependency-version: '7' dependency-type: direct:production update-type: version-update:semver-major dependency-group: github-actions ... Signed-off-by: dependabot[bot] --- .github/workflows/main.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5bbc689..5e4a8aa 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,26 +18,26 @@ jobs: uses: actions/checkout@v6 - name: Set up QEMU - uses: docker/setup-qemu-action@v3 + uses: docker/setup-qemu-action@v4 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Login to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@v7 with: context: . platforms: linux/amd64,linux/arm64 From 56826d33fd1acb2877d7e8235e56ff8af2ab3fd4 Mon Sep 17 00:00:00 2001 From: CyCl0ne Date: Mon, 9 Mar 2026 08:59:01 +0100 Subject: [PATCH 008/107] Add video codec selector and codec/quality columns in done list Allow users to prefer a specific video codec (H.264, H.265, AV1, VP9) when adding downloads. The selector filters available formats via yt-dlp format strings, falling back to best available if the preferred codec is not found. The completed downloads table now shows Quality and Codec columns. --- app/dl_formats.py | 16 ++++++++- app/main.py | 8 +++++ app/ytdl.py | 14 ++++++-- ui/angular.json | 3 +- ui/src/app/app.html | 27 +++++++++++++++ ui/src/app/app.ts | 44 ++++++++++++++++++++++-- ui/src/app/interfaces/download.ts | 1 + ui/src/app/services/downloads.service.ts | 6 +++- 8 files changed, 111 insertions(+), 8 deletions(-) diff --git a/app/dl_formats.py b/app/dl_formats.py index e94ec9a..283f53c 100644 --- a/app/dl_formats.py +++ b/app/dl_formats.py @@ -3,6 +3,13 @@ import copy AUDIO_FORMATS = ("m4a", "mp3", "opus", "wav", "flac") CAPTION_MODES = ("auto_only", "manual_only", "prefer_manual", "prefer_auto") +CODEC_FILTER_MAP = { + 'h264': "[vcodec~='^(h264|avc)']", + 'h265': "[vcodec~='^(h265|hevc)']", + 'av1': "[vcodec~='^av0?1']", + 'vp9': "[vcodec~='^vp0?9']", +} + def _normalize_caption_mode(mode: str) -> str: mode = (mode or "").strip() @@ -14,13 +21,14 @@ def _normalize_subtitle_language(language: str) -> str: return language or "en" -def get_format(format: str, quality: str) -> str: +def get_format(format: str, quality: str, video_codec: str = "auto") -> str: """ Returns format for download Args: format (str): format selected quality (str): quality selected + video_codec (str): video codec filter (auto, h264, h265, av1, vp9) Raises: Exception: unknown quality, unknown format @@ -52,6 +60,7 @@ def get_format(format: str, quality: str) -> str: vfmt, afmt = ("[ext=mp4]", "[ext=m4a]") if format == "mp4" else ("", "") vres = f"[height<={quality}]" if quality not in ("best", "best_ios", "worst") else "" vcombo = vres + vfmt + codec_filter = CODEC_FILTER_MAP.get(video_codec, "") if quality == "best_ios": # iOS has strict requirements for video files, requiring h264 or h265 @@ -61,6 +70,10 @@ def get_format(format: str, quality: str) -> str: # convert if needed), and falls back to getting the best available MP4 # file. return f"bestvideo[vcodec~='^((he|a)vc|h26[45])']{vres}+bestaudio[acodec=aac]/bestvideo[vcodec~='^((he|a)vc|h26[45])']{vres}+bestaudio{afmt}/bestvideo{vcombo}+bestaudio{afmt}/best{vcombo}" + + if codec_filter: + # Try codec-filtered first, fall back to unfiltered + return f"bestvideo{codec_filter}{vcombo}+bestaudio{afmt}/bestvideo{vcombo}+bestaudio{afmt}/best{vcombo}" return f"bestvideo{vcombo}+bestaudio{afmt}/best{vcombo}" raise Exception(f"Unkown format {format}") @@ -73,6 +86,7 @@ def get_opts( subtitle_format: str = "srt", subtitle_language: str = "en", subtitle_mode: str = "prefer_manual", + video_codec: str = "auto", ) -> dict: """ Returns extra download options diff --git a/app/main.py b/app/main.py index 74ac76a..b2d9f14 100644 --- a/app/main.py +++ b/app/main.py @@ -288,6 +288,7 @@ async def add(request): subtitle_format = post.get('subtitle_format') subtitle_language = post.get('subtitle_language') subtitle_mode = post.get('subtitle_mode') + video_codec = post.get('video_codec') if custom_name_prefix is None: custom_name_prefix = '' @@ -319,6 +320,12 @@ async def add(request): if subtitle_mode not in VALID_SUBTITLE_MODES: raise web.HTTPBadRequest(reason=f'subtitle_mode must be one of {sorted(VALID_SUBTITLE_MODES)}') + if video_codec is None: + video_codec = 'auto' + video_codec = str(video_codec).strip().lower() + if video_codec not in ('auto', 'h264', 'h265', 'av1', 'vp9'): + raise web.HTTPBadRequest(reason="video_codec must be one of ['auto', 'h264', 'h265', 'av1', 'vp9']") + playlist_item_limit = int(playlist_item_limit) status = await dqueue.add( @@ -334,6 +341,7 @@ async def add(request): subtitle_format, subtitle_language, subtitle_mode, + video_codec=video_codec, ) return web.Response(text=serializer.encode(status)) diff --git a/app/ytdl.py b/app/ytdl.py index f73eb36..370c458 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -162,6 +162,7 @@ class DownloadInfo: subtitle_format="srt", subtitle_language="en", subtitle_mode="prefer_manual", + video_codec="auto", ): self.id = id if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{id}' self.title = title if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{title}' @@ -184,6 +185,7 @@ class DownloadInfo: self.subtitle_language = subtitle_language self.subtitle_mode = subtitle_mode self.subtitle_files = [] + self.video_codec = video_codec class Download: manager = None @@ -194,7 +196,7 @@ class Download: self.output_template = output_template self.output_template_chapter = output_template_chapter self.info = info - self.format = get_format(format, quality) + self.format = get_format(format, quality, video_codec=getattr(info, 'video_codec', 'auto')) self.ytdl_opts = get_opts( format, quality, @@ -202,6 +204,7 @@ class Download: subtitle_format=getattr(info, 'subtitle_format', 'srt'), subtitle_language=getattr(info, 'subtitle_language', 'en'), subtitle_mode=getattr(info, 'subtitle_mode', 'prefer_manual'), + video_codec=getattr(info, 'video_codec', 'auto'), ) if "impersonate" in self.ytdl_opts: self.ytdl_opts["impersonate"] = yt_dlp.networking.impersonate.ImpersonateTarget.from_str(self.ytdl_opts["impersonate"]) @@ -687,6 +690,7 @@ class DownloadQueue: subtitle_mode, already, _add_gen=None, + video_codec="auto", ): if not entry: return {'status': 'error', 'msg': "Invalid/empty data was given."} @@ -718,6 +722,7 @@ class DownloadQueue: subtitle_mode, already, _add_gen, + video_codec, ) elif etype == 'playlist' or etype == 'channel': log.debug(f'Processing as a {etype}') @@ -757,6 +762,7 @@ class DownloadQueue: subtitle_mode, already, _add_gen, + video_codec, ) ) if any(res['status'] == 'error' for res in results): @@ -785,6 +791,7 @@ class DownloadQueue: subtitle_format, subtitle_language, subtitle_mode, + video_codec, ) await self.__add_download(dl, auto_start) return {'status': 'ok'} @@ -806,11 +813,13 @@ class DownloadQueue: subtitle_mode="prefer_manual", already=None, _add_gen=None, + video_codec="auto", ): log.info( f'adding {url}: {quality=} {format=} {already=} {folder=} {custom_name_prefix=} ' f'{playlist_item_limit=} {auto_start=} {split_by_chapters=} {chapter_template=} ' - f'{subtitle_format=} {subtitle_language=} {subtitle_mode=}' + f'{subtitle_format=} {subtitle_language=} {subtitle_mode=} ' + f'{video_codec=}' ) if already is None: _add_gen = self._add_generation @@ -840,6 +849,7 @@ class DownloadQueue: subtitle_mode, already, _add_gen, + video_codec, ) async def start_pending(self, ids): diff --git a/ui/angular.json b/ui/angular.json index 229711e..6f0fddb 100644 --- a/ui/angular.json +++ b/ui/angular.json @@ -77,7 +77,8 @@ "buildTarget": "metube:build:production" }, "development": { - "buildTarget": "metube:build:development" + "buildTarget": "metube:build:development", + "proxyConfig": "proxy.conf.json" } }, "defaultConfiguration": "development" diff --git a/ui/src/app/app.html b/ui/src/app/app.html index 202ee80..56a0b44 100644 --- a/ui/src/app/app.html +++ b/ui/src/app/app.html @@ -279,6 +279,23 @@
} + @if (isVideoType()) { +
+
+ Video Codec + +
+
+ }
+ + @if (chapterFile.size) { {{ chapterFile.size | fileSize }} diff --git a/ui/src/app/app.ts b/ui/src/app/app.ts index e34b635..0228f31 100644 --- a/ui/src/app/app.ts +++ b/ui/src/app/app.ts @@ -53,6 +53,7 @@ export class App implements AfterViewInit, OnInit { subtitleFormat: string; subtitleLanguage: string; subtitleMode: string; + videoCodec: string; addInProgress = false; cancelRequested = false; hasCookies = false; @@ -169,6 +170,13 @@ export class App implements AfterViewInit, OnInit { { id: 'manual_only', text: 'Manual Only' }, { id: 'auto_only', text: 'Auto Only' }, ]; + videoCodecs = [ + { id: 'auto', text: 'Auto' }, + { id: 'h264', text: 'H.264' }, + { id: 'h265', text: 'H.265 (HEVC)' }, + { id: 'av1', text: 'AV1' }, + { id: 'vp9', text: 'VP9' }, + ]; constructor() { this.format = this.cookieService.get('metube_format') || 'any'; @@ -182,6 +190,11 @@ export class App implements AfterViewInit, OnInit { this.subtitleFormat = this.cookieService.get('metube_subtitle_format') || 'srt'; this.subtitleLanguage = this.cookieService.get('metube_subtitle_language') || 'en'; this.subtitleMode = this.cookieService.get('metube_subtitle_mode') || 'prefer_manual'; + this.videoCodec = this.cookieService.get('metube_video_codec') || 'auto'; + const allowedVideoCodecs = new Set(this.videoCodecs.map(c => c.id)); + if (!allowedVideoCodecs.has(this.videoCodec)) { + this.videoCodec = 'auto'; + } const allowedSubtitleFormats = new Set(this.subtitleFormats.map(fmt => fmt.id)); const allowedSubtitleModes = new Set(this.subtitleModes.map(mode => mode.id)); if (!allowedSubtitleFormats.has(this.subtitleFormat)) { @@ -379,6 +392,28 @@ export class App implements AfterViewInit, OnInit { this.cookieService.set('metube_subtitle_mode', this.subtitleMode, { expires: 3650 }); } + videoCodecChanged() { + this.cookieService.set('metube_video_codec', this.videoCodec, { expires: 3650 }); + } + + isVideoType() { + return (this.format === 'any' || this.format === 'mp4') && this.quality !== 'audio'; + } + + formatQualityLabel(download: Download): string { + const q = download.quality; + if (!q) return ''; + if (/^\d+$/.test(q)) return `${q}p`; + if (q === 'best_ios') return 'Best (iOS)'; + return q.charAt(0).toUpperCase() + q.slice(1); + } + + formatCodecLabel(download: Download): string { + const codec = download.video_codec; + if (!codec || codec === 'auto') return 'Auto'; + return this.videoCodecs.find(c => c.id === codec)?.text ?? codec; + } + queueSelectionChanged(checked: number) { this.queueDelSelected().nativeElement.disabled = checked == 0; this.queueDownloadSelected().nativeElement.disabled = checked == 0; @@ -412,6 +447,7 @@ export class App implements AfterViewInit, OnInit { subtitleFormat?: string, subtitleLanguage?: string, subtitleMode?: string, + videoCodec?: string, ) { url = url ?? this.addUrl quality = quality ?? this.quality @@ -425,6 +461,7 @@ export class App implements AfterViewInit, OnInit { subtitleFormat = subtitleFormat ?? this.subtitleFormat subtitleLanguage = subtitleLanguage ?? this.subtitleLanguage subtitleMode = subtitleMode ?? this.subtitleMode + videoCodec = videoCodec ?? this.videoCodec // Validate chapter template if chapter splitting is enabled if (splitByChapters && !chapterTemplate.includes('%(section_number)')) { @@ -432,10 +469,10 @@ export class App implements AfterViewInit, OnInit { return; } - console.debug('Downloading: url=' + url + ' quality=' + quality + ' format=' + format + ' folder=' + folder + ' customNamePrefix=' + customNamePrefix + ' playlistItemLimit=' + playlistItemLimit + ' autoStart=' + autoStart + ' splitByChapters=' + splitByChapters + ' chapterTemplate=' + chapterTemplate + ' subtitleFormat=' + subtitleFormat + ' subtitleLanguage=' + subtitleLanguage + ' subtitleMode=' + subtitleMode); + console.debug('Downloading: url=' + url + ' quality=' + quality + ' format=' + format + ' folder=' + folder + ' customNamePrefix=' + customNamePrefix + ' playlistItemLimit=' + playlistItemLimit + ' autoStart=' + autoStart + ' splitByChapters=' + splitByChapters + ' chapterTemplate=' + chapterTemplate + ' subtitleFormat=' + subtitleFormat + ' subtitleLanguage=' + subtitleLanguage + ' subtitleMode=' + subtitleMode + ' videoCodec=' + videoCodec); this.addInProgress = true; this.cancelRequested = false; - this.downloads.add(url, quality, format, folder, customNamePrefix, playlistItemLimit, autoStart, splitByChapters, chapterTemplate, subtitleFormat, subtitleLanguage, subtitleMode).subscribe((status: Status) => { + this.downloads.add(url, quality, format, folder, customNamePrefix, playlistItemLimit, autoStart, splitByChapters, chapterTemplate, subtitleFormat, subtitleLanguage, subtitleMode, videoCodec).subscribe((status: Status) => { if (status.status === 'error' && !this.cancelRequested) { alert(`Error adding URL: ${status.msg}`); } else if (status.status !== 'error') { @@ -473,6 +510,7 @@ export class App implements AfterViewInit, OnInit { download.subtitle_format, download.subtitle_language, download.subtitle_mode, + download.video_codec, ); this.downloads.delById('done', [key]).subscribe(); } @@ -620,7 +658,7 @@ export class App implements AfterViewInit, OnInit { // Now pass the selected quality, format, folder, etc. to the add() method this.downloads.add(url, this.quality, this.format, this.folder, this.customNamePrefix, this.playlistItemLimit, this.autoStart, this.splitByChapters, this.chapterTemplate, - this.subtitleFormat, this.subtitleLanguage, this.subtitleMode) + this.subtitleFormat, this.subtitleLanguage, this.subtitleMode, this.videoCodec) .subscribe({ next: (status: Status) => { if (status.status === 'error') { diff --git a/ui/src/app/interfaces/download.ts b/ui/src/app/interfaces/download.ts index 7e709c2..6d69785 100644 --- a/ui/src/app/interfaces/download.ts +++ b/ui/src/app/interfaces/download.ts @@ -13,6 +13,7 @@ export interface Download { subtitle_format?: string; subtitle_language?: string; subtitle_mode?: string; + video_codec?: string; status: string; msg: string; percent: number; diff --git a/ui/src/app/services/downloads.service.ts b/ui/src/app/services/downloads.service.ts index 37f4dc7..94d629d 100644 --- a/ui/src/app/services/downloads.service.ts +++ b/ui/src/app/services/downloads.service.ts @@ -120,6 +120,7 @@ export class DownloadsService { subtitleFormat: string, subtitleLanguage: string, subtitleMode: string, + videoCodec: string, ) { return this.http.post('add', { url: url, @@ -133,7 +134,8 @@ export class DownloadsService { chapter_template: chapterTemplate, subtitle_format: subtitleFormat, subtitle_language: subtitleLanguage, - subtitle_mode: subtitleMode + subtitle_mode: subtitleMode, + video_codec: videoCodec, }).pipe( catchError(this.handleHTTPError) ); @@ -183,6 +185,7 @@ export class DownloadsService { const defaultSubtitleFormat = 'srt'; const defaultSubtitleLanguage = 'en'; const defaultSubtitleMode = 'prefer_manual'; + const defaultVideoCodec = 'auto'; return new Promise((resolve, reject) => { this.add( @@ -198,6 +201,7 @@ export class DownloadsService { defaultSubtitleFormat, defaultSubtitleLanguage, defaultSubtitleMode, + defaultVideoCodec, ) .subscribe({ next: (response) => resolve(response), From 5c321bfacab01200f54e068dad868457ef9001b6 Mon Sep 17 00:00:00 2001 From: Alex Shnitman Date: Fri, 13 Mar 2026 19:47:36 +0200 Subject: [PATCH 009/107] reoganize quality and codec selections --- app/dl_formats.py | 83 +++--- app/main.py | 127 +++++++-- app/ytdl.py | 149 +++++++---- ui/src/app/app.html | 325 +++++++++++++++-------- ui/src/app/app.ts | 286 +++++++++++++++----- ui/src/app/interfaces/download.ts | 6 +- ui/src/app/interfaces/formats.ts | 152 ++++++----- ui/src/app/services/downloads.service.ts | 22 +- 8 files changed, 766 insertions(+), 384 deletions(-) diff --git a/app/dl_formats.py b/app/dl_formats.py index 283f53c..e229366 100644 --- a/app/dl_formats.py +++ b/app/dl_formats.py @@ -21,91 +21,90 @@ def _normalize_subtitle_language(language: str) -> str: return language or "en" -def get_format(format: str, quality: str, video_codec: str = "auto") -> str: +def get_format(download_type: str, codec: str, format: str, quality: str) -> str: """ - Returns format for download + Returns yt-dlp format selector. Args: - format (str): format selected - quality (str): quality selected - video_codec (str): video codec filter (auto, h264, h265, av1, vp9) + download_type (str): selected content type (video, audio, captions, thumbnail) + codec (str): selected video codec (auto, h264, h265, av1, vp9) + format (str): selected output format/profile for type + quality (str): selected quality Raises: - Exception: unknown quality, unknown format + Exception: unknown type/format Returns: - dl_format: Formatted download string + str: yt-dlp format selector """ - format = format or "any" + download_type = (download_type or "video").strip().lower() + format = (format or "any").strip().lower() + codec = (codec or "auto").strip().lower() + quality = (quality or "best").strip().lower() if format.startswith("custom:"): return format[7:] - if format == "thumbnail": - # Quality is irrelevant in this case since we skip the download + if download_type == "thumbnail": return "bestaudio/best" - if format == "captions": - # Quality is irrelevant in this case since we skip the download + if download_type == "captions": return "bestaudio/best" - if format in AUDIO_FORMATS: - # Audio quality needs to be set post-download, set in opts + if download_type == "audio": + if format not in AUDIO_FORMATS: + raise Exception(f"Unknown audio format {format}") return f"bestaudio[ext={format}]/bestaudio/best" - if format in ("mp4", "any"): - if quality == "audio": - return "bestaudio/best" - # video {res} {vfmt} + audio {afmt} {res} {vfmt} - vfmt, afmt = ("[ext=mp4]", "[ext=m4a]") if format == "mp4" else ("", "") - vres = f"[height<={quality}]" if quality not in ("best", "best_ios", "worst") else "" + if download_type == "video": + if format not in ("any", "mp4", "ios"): + raise Exception(f"Unknown video format {format}") + vfmt, afmt = ("[ext=mp4]", "[ext=m4a]") if format in ("mp4", "ios") else ("", "") + vres = f"[height<={quality}]" if quality not in ("best", "worst") else "" vcombo = vres + vfmt - codec_filter = CODEC_FILTER_MAP.get(video_codec, "") + codec_filter = CODEC_FILTER_MAP.get(codec, "") - if quality == "best_ios": - # iOS has strict requirements for video files, requiring h264 or h265 - # video codec and aac audio codec in MP4 container. This format string - # attempts to get the fully compatible formats first, then the h264/h265 - # video codec with any M4A audio codec (because audio is faster to - # convert if needed), and falls back to getting the best available MP4 - # file. + if format == "ios": return f"bestvideo[vcodec~='^((he|a)vc|h26[45])']{vres}+bestaudio[acodec=aac]/bestvideo[vcodec~='^((he|a)vc|h26[45])']{vres}+bestaudio{afmt}/bestvideo{vcombo}+bestaudio{afmt}/best{vcombo}" if codec_filter: - # Try codec-filtered first, fall back to unfiltered return f"bestvideo{codec_filter}{vcombo}+bestaudio{afmt}/bestvideo{vcombo}+bestaudio{afmt}/best{vcombo}" return f"bestvideo{vcombo}+bestaudio{afmt}/best{vcombo}" - raise Exception(f"Unkown format {format}") + raise Exception(f"Unknown download_type {download_type}") def get_opts( + download_type: str, + codec: str, format: str, quality: str, ytdl_opts: dict, - subtitle_format: str = "srt", subtitle_language: str = "en", subtitle_mode: str = "prefer_manual", - video_codec: str = "auto", ) -> dict: """ - Returns extra download options - Mostly postprocessing options + Returns extra yt-dlp options/postprocessors. Args: - format (str): format selected - quality (str): quality of format selected (needed for some formats) + download_type (str): selected content type + codec (str): selected codec (unused currently, kept for API consistency) + format (str): selected format/profile + quality (str): selected quality ytdl_opts (dict): current options selected Returns: - ytdl_opts: Extra options + dict: extended options """ + del codec # kept for parity with get_format signature + download_type = (download_type or "video").strip().lower() + format = (format or "any").strip().lower() opts = copy.deepcopy(ytdl_opts) postprocessors = [] - if format in AUDIO_FORMATS: + if download_type == "audio": postprocessors.append( { "key": "FFmpegExtractAudio", @@ -114,7 +113,6 @@ def get_opts( } ) - # Audio formats without thumbnail if format not in ("wav") and "writethumbnail" not in opts: opts["writethumbnail"] = True postprocessors.append( @@ -127,19 +125,18 @@ def get_opts( postprocessors.append({"key": "FFmpegMetadata"}) postprocessors.append({"key": "EmbedThumbnail"}) - if format == "thumbnail": + if download_type == "thumbnail": opts["skip_download"] = True opts["writethumbnail"] = True postprocessors.append( {"key": "FFmpegThumbnailsConvertor", "format": "jpg", "when": "before_dl"} ) - if format == "captions": + if download_type == "captions": mode = _normalize_caption_mode(subtitle_mode) language = _normalize_subtitle_language(subtitle_language) opts["skip_download"] = True - requested_subtitle_format = (subtitle_format or "srt").lower() - # txt is a derived, non-timed format produced from SRT after download. + requested_subtitle_format = (format or "srt").lower() if requested_subtitle_format == "txt": requested_subtitle_format = "srt" opts["subtitlesformat"] = requested_subtitle_format diff --git a/app/main.py b/app/main.py index b2d9f14..6e3cbbf 100644 --- a/app/main.py +++ b/app/main.py @@ -194,6 +194,68 @@ routes = web.RouteTableDef() VALID_SUBTITLE_FORMATS = {'srt', 'txt', 'vtt', 'ttml', 'sbv', 'scc', 'dfxp'} VALID_SUBTITLE_MODES = {'auto_only', 'manual_only', 'prefer_manual', 'prefer_auto'} SUBTITLE_LANGUAGE_RE = re.compile(r'^[A-Za-z0-9][A-Za-z0-9-]{0,34}$') +VALID_DOWNLOAD_TYPES = {'video', 'audio', 'captions', 'thumbnail'} +VALID_VIDEO_CODECS = {'auto', 'h264', 'h265', 'av1', 'vp9'} +VALID_VIDEO_FORMATS = {'any', 'mp4', 'ios'} +VALID_AUDIO_FORMATS = {'m4a', 'mp3', 'opus', 'wav', 'flac'} +VALID_THUMBNAIL_FORMATS = {'jpg'} + + +def _migrate_legacy_request(post: dict) -> dict: + """ + BACKWARD COMPATIBILITY: Translate old API request schema into the new one. + + Old API: + format (any/mp4/m4a/mp3/opus/wav/flac/thumbnail/captions) + quality + video_codec + subtitle_format (only when format=captions) + + New API: + download_type (video/audio/captions/thumbnail) + codec + format + quality + """ + if "download_type" in post: + return post + + old_format = str(post.get("format") or "any").strip().lower() + old_quality = str(post.get("quality") or "best").strip().lower() + old_video_codec = str(post.get("video_codec") or "auto").strip().lower() + + if old_format in VALID_AUDIO_FORMATS: + post["download_type"] = "audio" + post["codec"] = "auto" + post["format"] = old_format + elif old_format == "thumbnail": + post["download_type"] = "thumbnail" + post["codec"] = "auto" + post["format"] = "jpg" + post["quality"] = "best" + elif old_format == "captions": + post["download_type"] = "captions" + post["codec"] = "auto" + post["format"] = str(post.get("subtitle_format") or "srt").strip().lower() + post["quality"] = "best" + else: + # old_format is usually any/mp4 (legacy video path) + post["download_type"] = "video" + post["codec"] = old_video_codec + if old_quality == "best_ios": + post["format"] = "ios" + post["quality"] = "best" + elif old_quality == "audio": + # Legacy "audio only" under video format maps to m4a audio. + post["download_type"] = "audio" + post["codec"] = "auto" + post["format"] = "m4a" + post["quality"] = "best" + else: + post["format"] = old_format + post["quality"] = old_quality + + return post class Notifier(DownloadQueueNotifier): async def added(self, dl): @@ -272,23 +334,24 @@ if config.YTDL_OPTIONS_FILE: async def add(request): log.info("Received request to add download") post = await request.json() + post = _migrate_legacy_request(post) log.info(f"Request data: {post}") url = post.get('url') - quality = post.get('quality') - if not url or not quality: - log.error("Bad request: missing 'url' or 'quality'") - raise web.HTTPBadRequest() + download_type = post.get('download_type') + codec = post.get('codec') format = post.get('format') + quality = post.get('quality') + if not url or not quality or not download_type: + log.error("Bad request: missing 'url', 'download_type', or 'quality'") + raise web.HTTPBadRequest() folder = post.get('folder') custom_name_prefix = post.get('custom_name_prefix') playlist_item_limit = post.get('playlist_item_limit') auto_start = post.get('auto_start') split_by_chapters = post.get('split_by_chapters') chapter_template = post.get('chapter_template') - subtitle_format = post.get('subtitle_format') subtitle_language = post.get('subtitle_language') subtitle_mode = post.get('subtitle_mode') - video_codec = post.get('video_codec') if custom_name_prefix is None: custom_name_prefix = '' @@ -302,46 +365,72 @@ async def add(request): split_by_chapters = False if chapter_template is None: chapter_template = config.OUTPUT_TEMPLATE_CHAPTER - if subtitle_format is None: - subtitle_format = 'srt' if subtitle_language is None: subtitle_language = 'en' if subtitle_mode is None: subtitle_mode = 'prefer_manual' - subtitle_format = str(subtitle_format).strip().lower() + download_type = str(download_type).strip().lower() + codec = str(codec or 'auto').strip().lower() + format = str(format or '').strip().lower() + quality = str(quality).strip().lower() subtitle_language = str(subtitle_language).strip() subtitle_mode = str(subtitle_mode).strip() + if chapter_template and ('..' in chapter_template or chapter_template.startswith('/') or chapter_template.startswith('\\')): raise web.HTTPBadRequest(reason='chapter_template must not contain ".." or start with a path separator') - if subtitle_format not in VALID_SUBTITLE_FORMATS: - raise web.HTTPBadRequest(reason=f'subtitle_format must be one of {sorted(VALID_SUBTITLE_FORMATS)}') if not SUBTITLE_LANGUAGE_RE.fullmatch(subtitle_language): raise web.HTTPBadRequest(reason='subtitle_language must match pattern [A-Za-z0-9-] and be at most 35 characters') if subtitle_mode not in VALID_SUBTITLE_MODES: raise web.HTTPBadRequest(reason=f'subtitle_mode must be one of {sorted(VALID_SUBTITLE_MODES)}') - if video_codec is None: - video_codec = 'auto' - video_codec = str(video_codec).strip().lower() - if video_codec not in ('auto', 'h264', 'h265', 'av1', 'vp9'): - raise web.HTTPBadRequest(reason="video_codec must be one of ['auto', 'h264', 'h265', 'av1', 'vp9']") + if download_type not in VALID_DOWNLOAD_TYPES: + raise web.HTTPBadRequest(reason=f'download_type must be one of {sorted(VALID_DOWNLOAD_TYPES)}') + if codec not in VALID_VIDEO_CODECS: + raise web.HTTPBadRequest(reason=f'codec must be one of {sorted(VALID_VIDEO_CODECS)}') + + if download_type == 'video': + if format not in VALID_VIDEO_FORMATS: + raise web.HTTPBadRequest(reason=f'format must be one of {sorted(VALID_VIDEO_FORMATS)} for video') + if quality not in {'best', 'worst', '2160', '1440', '1080', '720', '480', '360', '240'}: + raise web.HTTPBadRequest(reason="quality must be one of ['best', '2160', '1440', '1080', '720', '480', '360', '240', 'worst'] for video") + elif download_type == 'audio': + if format not in VALID_AUDIO_FORMATS: + raise web.HTTPBadRequest(reason=f'format must be one of {sorted(VALID_AUDIO_FORMATS)} for audio') + allowed_audio_qualities = {'best'} + if format == 'mp3': + allowed_audio_qualities |= {'320', '192', '128'} + elif format == 'm4a': + allowed_audio_qualities |= {'192', '128'} + if quality not in allowed_audio_qualities: + raise web.HTTPBadRequest(reason=f'quality must be one of {sorted(allowed_audio_qualities)} for format {format}') + codec = 'auto' + elif download_type == 'captions': + if format not in VALID_SUBTITLE_FORMATS: + raise web.HTTPBadRequest(reason=f'format must be one of {sorted(VALID_SUBTITLE_FORMATS)} for captions') + quality = 'best' + codec = 'auto' + elif download_type == 'thumbnail': + if format not in VALID_THUMBNAIL_FORMATS: + raise web.HTTPBadRequest(reason=f'format must be one of {sorted(VALID_THUMBNAIL_FORMATS)} for thumbnail') + quality = 'best' + codec = 'auto' playlist_item_limit = int(playlist_item_limit) status = await dqueue.add( url, - quality, + download_type, + codec, format, + quality, folder, custom_name_prefix, playlist_item_limit, auto_start, split_by_chapters, chapter_template, - subtitle_format, subtitle_language, subtitle_mode, - video_codec=video_codec, ) return web.Response(text=serializer.encode(status)) diff --git a/app/ytdl.py b/app/ytdl.py index 370c458..e738fe9 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -151,6 +151,8 @@ class DownloadInfo: title, url, quality, + download_type, + codec, format, folder, custom_name_prefix, @@ -159,15 +161,15 @@ class DownloadInfo: playlist_item_limit, split_by_chapters, chapter_template, - subtitle_format="srt", subtitle_language="en", subtitle_mode="prefer_manual", - video_codec="auto", ): self.id = id if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{id}' self.title = title if len(custom_name_prefix) == 0 else f'{custom_name_prefix}.{title}' self.url = url self.quality = quality + self.download_type = download_type + self.codec = codec self.format = format self.folder = folder self.custom_name_prefix = custom_name_prefix @@ -181,11 +183,48 @@ class DownloadInfo: self.playlist_item_limit = playlist_item_limit self.split_by_chapters = split_by_chapters self.chapter_template = chapter_template - self.subtitle_format = subtitle_format self.subtitle_language = subtitle_language self.subtitle_mode = subtitle_mode self.subtitle_files = [] - self.video_codec = video_codec + + def __setstate__(self, state): + """BACKWARD COMPATIBILITY: migrate old DownloadInfo from persistent queue files.""" + self.__dict__.update(state) + if 'download_type' not in state: + old_format = state.get('format', 'any') + old_video_codec = state.get('video_codec', 'auto') + old_quality = state.get('quality', 'best') + old_subtitle_format = state.get('subtitle_format', 'srt') + + if old_format in AUDIO_FORMATS: + self.download_type = 'audio' + self.codec = 'auto' + elif old_format == 'thumbnail': + self.download_type = 'thumbnail' + self.codec = 'auto' + self.format = 'jpg' + elif old_format == 'captions': + self.download_type = 'captions' + self.codec = 'auto' + self.format = old_subtitle_format + else: + self.download_type = 'video' + self.codec = old_video_codec + if old_quality == 'best_ios': + self.format = 'ios' + self.quality = 'best' + elif old_quality == 'audio': + self.download_type = 'audio' + self.codec = 'auto' + self.format = 'm4a' + self.quality = 'best' + self.__dict__.pop('video_codec', None) + self.__dict__.pop('subtitle_format', None) + + if not getattr(self, "codec", None): + self.codec = "auto" + if not hasattr(self, "subtitle_files"): + self.subtitle_files = [] class Download: manager = None @@ -196,15 +235,20 @@ class Download: self.output_template = output_template self.output_template_chapter = output_template_chapter self.info = info - self.format = get_format(format, quality, video_codec=getattr(info, 'video_codec', 'auto')) + self.format = get_format( + getattr(info, 'download_type', 'video'), + getattr(info, 'codec', 'auto'), + format, + quality, + ) self.ytdl_opts = get_opts( + getattr(info, 'download_type', 'video'), + getattr(info, 'codec', 'auto'), format, quality, ytdl_opts, - subtitle_format=getattr(info, 'subtitle_format', 'srt'), subtitle_language=getattr(info, 'subtitle_language', 'en'), subtitle_mode=getattr(info, 'subtitle_mode', 'prefer_manual'), - video_codec=getattr(info, 'video_codec', 'auto'), ) if "impersonate" in self.ytdl_opts: self.ytdl_opts["impersonate"] = yt_dlp.networking.impersonate.ImpersonateTarget.from_str(self.ytdl_opts["impersonate"]) @@ -244,7 +288,7 @@ class Download: # For captions-only downloads, yt-dlp may still report a media-like # filepath in MoveFiles. Capture subtitle outputs explicitly so the # UI can link to real caption files. - if self.info.format == 'captions': + if getattr(self.info, 'download_type', '') == 'captions': requested_subtitles = d.get('info_dict', {}).get('requested_subtitles', {}) or {} for subtitle in requested_subtitles.values(): if isinstance(subtitle, dict) and subtitle.get('filepath'): @@ -352,14 +396,14 @@ class Download: rel_name = os.path.relpath(fileName, self.download_dir) # For captions mode, ignore media-like placeholders and let subtitle_file # statuses define the final file shown in the UI. - if self.info.format == 'captions': - requested_subtitle_format = str(getattr(self.info, 'subtitle_format', '')).lower() + if getattr(self.info, 'download_type', '') == 'captions': + requested_subtitle_format = str(getattr(self.info, 'format', '')).lower() allowed_caption_exts = ('.txt',) if requested_subtitle_format == 'txt' else ('.vtt', '.srt', '.sbv', '.scc', '.ttml', '.dfxp') if not rel_name.lower().endswith(allowed_caption_exts): continue self.info.filename = rel_name self.info.size = os.path.getsize(fileName) if os.path.exists(fileName) else None - if self.info.format == 'thumbnail': + if getattr(self.info, 'download_type', '') == 'thumbnail': self.info.filename = re.sub(r'\.webm$', '.jpg', self.info.filename) # Handle chapter files @@ -384,7 +428,7 @@ class Download: subtitle_output_file = subtitle_file # txt mode is derived from SRT by stripping cue metadata. - if self.info.format == 'captions' and str(getattr(self.info, 'subtitle_format', '')).lower() == 'txt': + if getattr(self.info, 'download_type', '') == 'captions' and str(getattr(self.info, 'format', '')).lower() == 'txt': converted_txt = _convert_srt_to_txt_file(subtitle_file) if converted_txt: subtitle_output_file = converted_txt @@ -400,9 +444,9 @@ class Download: if not existing: self.info.subtitle_files.append({'filename': rel_path, 'size': file_size}) # Prefer first subtitle file as the primary result link in captions mode. - if self.info.format == 'captions' and ( + if getattr(self.info, 'download_type', '') == 'captions' and ( not getattr(self.info, 'filename', None) or - str(getattr(self.info, 'subtitle_format', '')).lower() == 'txt' + str(getattr(self.info, 'format', '')).lower() == 'txt' ): self.info.filename = rel_path self.info.size = file_size @@ -434,7 +478,7 @@ class PersistentQueue: def load(self): for k, v in self.saved_items(): - self.dict[k] = Download(None, None, None, None, None, None, {}, v) + self.dict[k] = Download(None, None, None, None, getattr(v, 'quality', 'best'), getattr(v, 'format', 'any'), {}, v) def exists(self, key): return key in self.dict @@ -625,8 +669,8 @@ class DownloadQueue: **({'impersonate': yt_dlp.networking.impersonate.ImpersonateTarget.from_str(self.config.YTDL_OPTIONS['impersonate'])} if 'impersonate' in self.config.YTDL_OPTIONS else {}), }).extract_info(url, download=False) - def __calc_download_path(self, quality, format, folder): - base_directory = self.config.DOWNLOAD_DIR if (quality != 'audio' and format not in AUDIO_FORMATS) else self.config.AUDIO_DOWNLOAD_DIR + def __calc_download_path(self, download_type, folder): + base_directory = self.config.AUDIO_DOWNLOAD_DIR if download_type == 'audio' else self.config.DOWNLOAD_DIR if folder: if not self.config.CUSTOM_DIRS: return None, {'status': 'error', 'msg': 'A folder for the download was specified but CUSTOM_DIRS is not true in the configuration.'} @@ -643,7 +687,7 @@ class DownloadQueue: return dldirectory, None async def __add_download(self, dl, auto_start): - dldirectory, error_message = self.__calc_download_path(dl.quality, dl.format, dl.folder) + dldirectory, error_message = self.__calc_download_path(dl.download_type, dl.folder) if error_message is not None: return error_message output = self.config.OUTPUT_TEMPLATE if len(dl.custom_name_prefix) == 0 else f'{dl.custom_name_prefix}.{self.config.OUTPUT_TEMPLATE}' @@ -677,20 +721,20 @@ class DownloadQueue: async def __add_entry( self, entry, - quality, + download_type, + codec, format, + quality, folder, custom_name_prefix, playlist_item_limit, auto_start, split_by_chapters, chapter_template, - subtitle_format, subtitle_language, subtitle_mode, already, _add_gen=None, - video_codec="auto", ): if not entry: return {'status': 'error', 'msg': "Invalid/empty data was given."} @@ -709,20 +753,20 @@ class DownloadQueue: log.debug('Processing as a url') return await self.add( entry['url'], - quality, + download_type, + codec, format, + quality, folder, custom_name_prefix, playlist_item_limit, auto_start, split_by_chapters, chapter_template, - subtitle_format, subtitle_language, subtitle_mode, already, _add_gen, - video_codec, ) elif etype == 'playlist' or etype == 'channel': log.debug(f'Processing as a {etype}') @@ -749,20 +793,20 @@ class DownloadQueue: results.append( await self.__add_entry( etr, - quality, + download_type, + codec, format, + quality, folder, custom_name_prefix, playlist_item_limit, auto_start, split_by_chapters, chapter_template, - subtitle_format, subtitle_language, subtitle_mode, already, _add_gen, - video_codec, ) ) if any(res['status'] == 'error' for res in results): @@ -776,22 +820,22 @@ class DownloadQueue: return {'status': 'ok'} if not self.queue.exists(key): dl = DownloadInfo( - entry['id'], - entry.get('title') or entry['id'], - key, - quality, - format, - folder, - custom_name_prefix, - error, - entry, - playlist_item_limit, - split_by_chapters, - chapter_template, - subtitle_format, - subtitle_language, - subtitle_mode, - video_codec, + id=entry['id'], + title=entry.get('title') or entry['id'], + url=key, + quality=quality, + download_type=download_type, + codec=codec, + format=format, + folder=folder, + custom_name_prefix=custom_name_prefix, + error=error, + entry=entry, + playlist_item_limit=playlist_item_limit, + split_by_chapters=split_by_chapters, + chapter_template=chapter_template, + subtitle_language=subtitle_language, + subtitle_mode=subtitle_mode, ) await self.__add_download(dl, auto_start) return {'status': 'ok'} @@ -800,26 +844,25 @@ class DownloadQueue: async def add( self, url, - quality, + download_type, + codec, format, + quality, folder, custom_name_prefix, playlist_item_limit, auto_start=True, split_by_chapters=False, chapter_template=None, - subtitle_format="srt", subtitle_language="en", subtitle_mode="prefer_manual", already=None, _add_gen=None, - video_codec="auto", ): log.info( - f'adding {url}: {quality=} {format=} {already=} {folder=} {custom_name_prefix=} ' + f'adding {url}: {download_type=} {codec=} {format=} {quality=} {already=} {folder=} {custom_name_prefix=} ' f'{playlist_item_limit=} {auto_start=} {split_by_chapters=} {chapter_template=} ' - f'{subtitle_format=} {subtitle_language=} {subtitle_mode=} ' - f'{video_codec=}' + f'{subtitle_language=} {subtitle_mode=}' ) if already is None: _add_gen = self._add_generation @@ -836,20 +879,20 @@ class DownloadQueue: return {'status': 'error', 'msg': str(exc)} return await self.__add_entry( entry, - quality, + download_type, + codec, format, + quality, folder, custom_name_prefix, playlist_item_limit, auto_start, split_by_chapters, chapter_template, - subtitle_format, subtitle_language, subtitle_mode, already, _add_gen, - video_codec, ) async def start_pending(self, ids): @@ -889,7 +932,7 @@ class DownloadQueue: if self.config.DELETE_FILE_ON_TRASHCAN: dl = self.done.get(id) try: - dldirectory, _ = self.__calc_download_path(dl.info.quality, dl.info.format, dl.info.folder) + dldirectory, _ = self.__calc_download_path(dl.info.download_type, dl.info.folder) os.remove(os.path.join(dldirectory, dl.info.filename)) except Exception as e: log.warn(f'deleting file for download {id} failed with error message {e!r}') diff --git a/ui/src/app/app.html b/ui/src/app/app.html index 56a0b44..9c3b8d7 100644 --- a/ui/src/app/app.html +++ b/ui/src/app/app.html @@ -120,39 +120,205 @@
-
-
- Quality - + @if (downloadType === 'video') { +
+
+ Type + +
-
-
-
- Format - +
+
+ Codec + +
-
-
+
+
+ Format + +
+
+
+
+ Quality + +
+
+ } @else if (downloadType === 'audio') { +
+
+ Type + +
+
+
+
+ Format + +
+
+
+
+ Quality + +
+
+ } @else if (downloadType === 'captions') { +
+
+ Type + +
+
+
+
+ Format + +
+
+
+
+ Language + + + @for (lang of subtitleLanguages; track lang.id) { + + } + +
+
+
+
+ Subtitle Source + +
+
+ } @else { +
+
+ Type + +
+
+
+
+ Format + +
+
+ } +
+ +
+
@@ -161,7 +327,7 @@
-
+
@@ -225,77 +391,6 @@ ngbTooltip="Maximum number of items to download from a playlist or channel (0 = no limit)">
- @if (format === 'captions') { -
-
- Subtitles - -
- @if (subtitleFormat === 'txt') { -
TXT is generated from SRT by stripping timestamps and cue numbers.
- } -
-
-
- Language - - - @for (lang of subtitleLanguages; track lang.id) { - - } - -
-
-
-
- Subtitle Source - -
-
- } - @if (isVideoType()) { -
-
- Video Codec - -
-
- }
@@ -506,8 +601,9 @@ Video + Type Quality - Codec + Codec / Format File Size Downloaded @@ -536,15 +632,18 @@ @if (!!entry[1].filename) { {{ entry[1].title }} } @else { - - {{entry[1].title}} - @if (entry[1].status === 'error' && !isErrorExpanded(entry[0])) { - - Click for details - - } - + @if (entry[1].status === 'error') { + + } @else { + {{entry[1].title}} + } } @if (entry[1].status === 'error' && isErrorExpanded(entry[0])) {
@@ -571,6 +670,9 @@
} + + {{ downloadTypeLabel(entry[1]) }} + {{ formatQualityLabel(entry[1]) }} @@ -613,6 +715,7 @@ + @if (chapterFile.size) { {{ chapterFile.size | fileSize }} diff --git a/ui/src/app/app.ts b/ui/src/app/app.ts index 0228f31..a890ffd 100644 --- a/ui/src/app/app.ts +++ b/ui/src/app/app.ts @@ -6,12 +6,27 @@ import { FormsModule } from '@angular/forms'; import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgSelectModule } from '@ng-select/ng-select'; -import { faTrashAlt, faCheckCircle, faTimesCircle, faRedoAlt, faSun, faMoon, faCheck, faCircleHalfStroke, faDownload, faExternalLinkAlt, faFileImport, faFileExport, faCopy, faClock, faTachometerAlt, faSortAmountDown, faSortAmountUp, faChevronRight, faUpload } from '@fortawesome/free-solid-svg-icons'; +import { faTrashAlt, faCheckCircle, faTimesCircle, faRedoAlt, faSun, faMoon, faCheck, faCircleHalfStroke, faDownload, faExternalLinkAlt, faFileImport, faFileExport, faCopy, faClock, faTachometerAlt, faSortAmountDown, faSortAmountUp, faChevronRight, faChevronDown, faUpload } from '@fortawesome/free-solid-svg-icons'; import { faGithub } from '@fortawesome/free-brands-svg-icons'; import { CookieService } from 'ngx-cookie-service'; import { DownloadsService } from './services/downloads.service'; import { Themes } from './theme'; -import { Download, Status, Theme , Quality, Format, Formats, State } from './interfaces'; +import { + Download, + Status, + Theme, + Quality, + Option, + AudioFormatOption, + DOWNLOAD_TYPES, + VIDEO_CODECS, + VIDEO_FORMATS, + VIDEO_QUALITIES, + AUDIO_FORMATS, + CAPTION_FORMATS, + THUMBNAIL_FORMATS, + State, +} from './interfaces'; import { EtaPipe, SpeedPipe, FileSizePipe } from './pipes'; import { MasterCheckboxComponent , SlaveCheckboxComponent} from './components/'; @@ -40,8 +55,15 @@ export class App implements AfterViewInit, OnInit { private http = inject(HttpClient); addUrl!: string; - formats: Format[] = Formats; + downloadTypes: Option[] = DOWNLOAD_TYPES; + videoCodecs: Option[] = VIDEO_CODECS; + videoFormats: Option[] = VIDEO_FORMATS; + audioFormats: AudioFormatOption[] = AUDIO_FORMATS; + captionFormats: Option[] = CAPTION_FORMATS; + thumbnailFormats: Option[] = THUMBNAIL_FORMATS; qualities!: Quality[]; + downloadType: string; + codec: string; quality: string; format: string; folder!: string; @@ -50,10 +72,8 @@ export class App implements AfterViewInit, OnInit { playlistItemLimit!: number; splitByChapters: boolean; chapterTemplate: string; - subtitleFormat: string; subtitleLanguage: string; subtitleMode: string; - videoCodec: string; addInProgress = false; cancelRequested = false; hasCookies = false; @@ -72,9 +92,18 @@ export class App implements AfterViewInit, OnInit { metubeVersion: string | null = null; isAdvancedOpen = false; sortAscending = false; - expandedErrors: Set = new Set(); + expandedErrors: Set = new Set(); cachedSortedDone: [string, Download][] = []; lastCopiedErrorId: string | null = null; + private previousDownloadType = 'video'; + private selectionsByType: Record = {}; + private readonly selectionCookiePrefix = 'metube_selection_'; // Download metrics activeDownloads = 0; @@ -112,13 +141,8 @@ export class App implements AfterViewInit, OnInit { faSortAmountDown = faSortAmountDown; faSortAmountUp = faSortAmountUp; faChevronRight = faChevronRight; + faChevronDown = faChevronDown; faUpload = faUpload; - subtitleFormats = [ - { id: 'srt', text: 'SRT' }, - { id: 'txt', text: 'TXT (Text only)' }, - { id: 'vtt', text: 'VTT' }, - { id: 'ttml', text: 'TTML' } - ]; subtitleLanguages = [ { id: 'en', text: 'English' }, { id: 'ar', text: 'Arabic' }, @@ -170,39 +194,35 @@ export class App implements AfterViewInit, OnInit { { id: 'manual_only', text: 'Manual Only' }, { id: 'auto_only', text: 'Auto Only' }, ]; - videoCodecs = [ - { id: 'auto', text: 'Auto' }, - { id: 'h264', text: 'H.264' }, - { id: 'h265', text: 'H.265 (HEVC)' }, - { id: 'av1', text: 'AV1' }, - { id: 'vp9', text: 'VP9' }, - ]; - constructor() { + this.downloadType = this.cookieService.get('metube_download_type') || 'video'; + this.codec = this.cookieService.get('metube_codec') || 'auto'; this.format = this.cookieService.get('metube_format') || 'any'; - // Needs to be set or qualities won't automatically be set - this.setQualities() this.quality = this.cookieService.get('metube_quality') || 'best'; this.autoStart = this.cookieService.get('metube_auto_start') !== 'false'; this.splitByChapters = this.cookieService.get('metube_split_chapters') === 'true'; // Will be set from backend configuration, use empty string as placeholder this.chapterTemplate = this.cookieService.get('metube_chapter_template') || ''; - this.subtitleFormat = this.cookieService.get('metube_subtitle_format') || 'srt'; this.subtitleLanguage = this.cookieService.get('metube_subtitle_language') || 'en'; this.subtitleMode = this.cookieService.get('metube_subtitle_mode') || 'prefer_manual'; - this.videoCodec = this.cookieService.get('metube_video_codec') || 'auto'; + const allowedDownloadTypes = new Set(this.downloadTypes.map(t => t.id)); const allowedVideoCodecs = new Set(this.videoCodecs.map(c => c.id)); - if (!allowedVideoCodecs.has(this.videoCodec)) { - this.videoCodec = 'auto'; + if (!allowedDownloadTypes.has(this.downloadType)) { + this.downloadType = 'video'; + } + if (!allowedVideoCodecs.has(this.codec)) { + this.codec = 'auto'; } - const allowedSubtitleFormats = new Set(this.subtitleFormats.map(fmt => fmt.id)); const allowedSubtitleModes = new Set(this.subtitleModes.map(mode => mode.id)); - if (!allowedSubtitleFormats.has(this.subtitleFormat)) { - this.subtitleFormat = 'srt'; - } if (!allowedSubtitleModes.has(this.subtitleMode)) { this.subtitleMode = 'prefer_manual'; } + this.loadSavedSelections(); + this.restoreSelection(this.downloadType); + this.normalizeSelectionsForType(); + this.setQualities(); + this.previousDownloadType = this.downloadType; + this.saveSelection(this.downloadType); this.sortAscending = this.cookieService.get('metube_sort_ascending') === 'true'; this.activeTheme = this.getPreferredTheme(this.cookieService); @@ -223,7 +243,7 @@ export class App implements AfterViewInit, OnInit { ngOnInit() { this.downloads.getCookieStatus().subscribe(data => { - this.hasCookies = data?.has_cookies || false; + this.hasCookies = !!(data && typeof data === 'object' && 'has_cookies' in data && data.has_cookies); }); this.getConfiguration(); this.getYtdlOptionsUpdateTime(); @@ -268,10 +288,27 @@ export class App implements AfterViewInit, OnInit { qualityChanged() { this.cookieService.set('metube_quality', this.quality, { expires: 3650 }); + this.saveSelection(this.downloadType); // Re-trigger custom directory change this.downloads.customDirsChanged.next(this.downloads.customDirs); } + downloadTypeChanged() { + this.saveSelection(this.previousDownloadType); + this.restoreSelection(this.downloadType); + this.cookieService.set('metube_download_type', this.downloadType, { expires: 3650 }); + this.normalizeSelectionsForType(false); + this.setQualities(); + this.saveSelection(this.downloadType); + this.previousDownloadType = this.downloadType; + this.downloads.customDirsChanged.next(this.downloads.customDirs); + } + + codecChanged() { + this.cookieService.set('metube_codec', this.codec, { expires: 3650 }); + this.saveSelection(this.downloadType); + } + showAdvanced() { return this.downloads.configuration['CUSTOM_DIRS']; } @@ -284,7 +321,7 @@ export class App implements AfterViewInit, OnInit { } isAudioType() { - return this.quality == 'audio' || this.format == 'mp3' || this.format == 'm4a' || this.format == 'opus' || this.format == 'wav' || this.format == 'flac'; + return this.downloadType === 'audio'; } getMatchingCustomDir() : Observable { @@ -358,8 +395,8 @@ export class App implements AfterViewInit, OnInit { formatChanged() { this.cookieService.set('metube_format', this.format, { expires: 3650 }); - // Updates to use qualities available - this.setQualities() + this.setQualities(); + this.saveSelection(this.downloadType); // Re-trigger custom directory change this.downloads.customDirsChanged.next(this.downloads.customDirs); } @@ -380,36 +417,42 @@ export class App implements AfterViewInit, OnInit { this.cookieService.set('metube_chapter_template', this.chapterTemplate, { expires: 3650 }); } - subtitleFormatChanged() { - this.cookieService.set('metube_subtitle_format', this.subtitleFormat, { expires: 3650 }); - } - subtitleLanguageChanged() { this.cookieService.set('metube_subtitle_language', this.subtitleLanguage, { expires: 3650 }); + this.saveSelection(this.downloadType); } subtitleModeChanged() { this.cookieService.set('metube_subtitle_mode', this.subtitleMode, { expires: 3650 }); - } - - videoCodecChanged() { - this.cookieService.set('metube_video_codec', this.videoCodec, { expires: 3650 }); + this.saveSelection(this.downloadType); } isVideoType() { - return (this.format === 'any' || this.format === 'mp4') && this.quality !== 'audio'; + return this.downloadType === 'video'; } formatQualityLabel(download: Download): string { + if (download.download_type === 'captions' || download.download_type === 'thumbnail') { + return '-'; + } const q = download.quality; if (!q) return ''; + if (/^\d+$/.test(q) && download.download_type === 'audio') return `${q} kbps`; if (/^\d+$/.test(q)) return `${q}p`; - if (q === 'best_ios') return 'Best (iOS)'; return q.charAt(0).toUpperCase() + q.slice(1); } + downloadTypeLabel(download: Download): string { + const type = download.download_type || 'video'; + return type.charAt(0).toUpperCase() + type.slice(1); + } + formatCodecLabel(download: Download): string { - const codec = download.video_codec; + if (download.download_type !== 'video') { + const format = (download.format || '').toUpperCase(); + return format || '-'; + } + const codec = download.codec; if (!codec || codec === 'auto') return 'Auto'; return this.videoCodecs.find(c => c.id === codec)?.text ?? codec; } @@ -425,17 +468,130 @@ export class App implements AfterViewInit, OnInit { } setQualities() { - // qualities for specific format - const format = this.formats.find(el => el.id == this.format) - if (format) { - this.qualities = format.qualities - const exists = this.qualities.find(el => el.id === this.quality) - this.quality = exists ? this.quality : 'best' + if (this.downloadType === 'video') { + this.qualities = this.format === 'ios' + ? [{ id: 'best', text: 'Best' }] + : VIDEO_QUALITIES; + } else if (this.downloadType === 'audio') { + const selectedFormat = this.audioFormats.find(el => el.id === this.format); + this.qualities = selectedFormat ? selectedFormat.qualities : [{ id: 'best', text: 'Best' }]; + } else { + this.qualities = [{ id: 'best', text: 'Best' }]; + } + const exists = this.qualities.find(el => el.id === this.quality); + this.quality = exists ? this.quality : 'best'; + } + + showCodecSelector() { + return this.downloadType === 'video'; + } + + showFormatSelector() { + return this.downloadType !== 'thumbnail'; + } + + showQualitySelector() { + if (this.downloadType === 'video') { + return this.format !== 'ios'; + } + return this.downloadType === 'audio'; + } + + getFormatOptions() { + if (this.downloadType === 'video') { + return this.videoFormats; + } + if (this.downloadType === 'audio') { + return this.audioFormats; + } + if (this.downloadType === 'captions') { + return this.captionFormats; + } + return this.thumbnailFormats; + } + + private normalizeSelectionsForType(resetForTypeChange = false) { + if (this.downloadType === 'video') { + const allowedFormats = new Set(this.videoFormats.map(f => f.id)); + if (resetForTypeChange || !allowedFormats.has(this.format)) { + this.format = 'any'; + } + const allowedCodecs = new Set(this.videoCodecs.map(c => c.id)); + if (resetForTypeChange || !allowedCodecs.has(this.codec)) { + this.codec = 'auto'; + } + } else if (this.downloadType === 'audio') { + const allowedFormats = new Set(this.audioFormats.map(f => f.id)); + if (resetForTypeChange || !allowedFormats.has(this.format)) { + this.format = this.audioFormats[0].id; + } + } else if (this.downloadType === 'captions') { + const allowedFormats = new Set(this.captionFormats.map(f => f.id)); + if (resetForTypeChange || !allowedFormats.has(this.format)) { + this.format = 'srt'; + } + this.quality = 'best'; + } else { + this.format = 'jpg'; + this.quality = 'best'; + } + this.cookieService.set('metube_format', this.format, { expires: 3650 }); + this.cookieService.set('metube_codec', this.codec, { expires: 3650 }); + } + + private saveSelection(type: string) { + if (!type) return; + const selection = { + codec: this.codec, + format: this.format, + quality: this.quality, + subtitleLanguage: this.subtitleLanguage, + subtitleMode: this.subtitleMode, + }; + this.selectionsByType[type] = selection; + this.cookieService.set( + this.selectionCookiePrefix + type, + JSON.stringify(selection), + { expires: 3650 } + ); + } + + private restoreSelection(type: string) { + const saved = this.selectionsByType[type]; + if (!saved) return; + this.codec = saved.codec; + this.format = saved.format; + this.quality = saved.quality; + this.subtitleLanguage = saved.subtitleLanguage; + this.subtitleMode = saved.subtitleMode; + } + + private loadSavedSelections() { + for (const type of this.downloadTypes.map(t => t.id)) { + const key = this.selectionCookiePrefix + type; + if (!this.cookieService.check(key)) continue; + try { + const raw = this.cookieService.get(key); + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === 'object') { + this.selectionsByType[type] = { + codec: String(parsed.codec ?? 'auto'), + format: String(parsed.format ?? ''), + quality: String(parsed.quality ?? 'best'), + subtitleLanguage: String(parsed.subtitleLanguage ?? 'en'), + subtitleMode: String(parsed.subtitleMode ?? 'prefer_manual'), + }; + } + } catch { + // Ignore malformed cookie values. + } + } } -} addDownload( url?: string, + downloadType?: string, + codec?: string, quality?: string, format?: string, folder?: string, @@ -444,12 +600,12 @@ export class App implements AfterViewInit, OnInit { autoStart?: boolean, splitByChapters?: boolean, chapterTemplate?: string, - subtitleFormat?: string, subtitleLanguage?: string, subtitleMode?: string, - videoCodec?: string, ) { url = url ?? this.addUrl + downloadType = downloadType ?? this.downloadType + codec = codec ?? this.codec quality = quality ?? this.quality format = format ?? this.format folder = folder ?? this.folder @@ -458,10 +614,8 @@ export class App implements AfterViewInit, OnInit { autoStart = autoStart ?? this.autoStart splitByChapters = splitByChapters ?? this.splitByChapters chapterTemplate = chapterTemplate ?? this.chapterTemplate - subtitleFormat = subtitleFormat ?? this.subtitleFormat subtitleLanguage = subtitleLanguage ?? this.subtitleLanguage subtitleMode = subtitleMode ?? this.subtitleMode - videoCodec = videoCodec ?? this.videoCodec // Validate chapter template if chapter splitting is enabled if (splitByChapters && !chapterTemplate.includes('%(section_number)')) { @@ -469,10 +623,10 @@ export class App implements AfterViewInit, OnInit { return; } - console.debug('Downloading: url=' + url + ' quality=' + quality + ' format=' + format + ' folder=' + folder + ' customNamePrefix=' + customNamePrefix + ' playlistItemLimit=' + playlistItemLimit + ' autoStart=' + autoStart + ' splitByChapters=' + splitByChapters + ' chapterTemplate=' + chapterTemplate + ' subtitleFormat=' + subtitleFormat + ' subtitleLanguage=' + subtitleLanguage + ' subtitleMode=' + subtitleMode + ' videoCodec=' + videoCodec); + console.debug('Downloading: url=' + url + ' downloadType=' + downloadType + ' codec=' + codec + ' quality=' + quality + ' format=' + format + ' folder=' + folder + ' customNamePrefix=' + customNamePrefix + ' playlistItemLimit=' + playlistItemLimit + ' autoStart=' + autoStart + ' splitByChapters=' + splitByChapters + ' chapterTemplate=' + chapterTemplate + ' subtitleLanguage=' + subtitleLanguage + ' subtitleMode=' + subtitleMode); this.addInProgress = true; this.cancelRequested = false; - this.downloads.add(url, quality, format, folder, customNamePrefix, playlistItemLimit, autoStart, splitByChapters, chapterTemplate, subtitleFormat, subtitleLanguage, subtitleMode, videoCodec).subscribe((status: Status) => { + this.downloads.add(url, downloadType, codec, quality, format, folder, customNamePrefix, playlistItemLimit, autoStart, splitByChapters, chapterTemplate, subtitleLanguage, subtitleMode).subscribe((status: Status) => { if (status.status === 'error' && !this.cancelRequested) { alert(`Error adding URL: ${status.msg}`); } else if (status.status !== 'error') { @@ -499,6 +653,8 @@ export class App implements AfterViewInit, OnInit { retryDownload(key: string, download: Download) { this.addDownload( download.url, + download.download_type, + download.codec, download.quality, download.format, download.folder, @@ -507,10 +663,8 @@ export class App implements AfterViewInit, OnInit { true, download.split_by_chapters, download.chapter_template, - download.subtitle_format, download.subtitle_language, download.subtitle_mode, - download.video_codec, ); this.downloads.delById('done', [key]).subscribe(); } @@ -560,7 +714,7 @@ export class App implements AfterViewInit, OnInit { buildDownloadLink(download: Download) { let baseDir = this.downloads.configuration["PUBLIC_HOST_URL"]; - if (download.quality == 'audio' || download.filename.endsWith('.mp3')) { + if (download.download_type === 'audio' || download.filename.endsWith('.mp3')) { baseDir = this.downloads.configuration["PUBLIC_HOST_AUDIO_URL"]; } @@ -584,7 +738,7 @@ export class App implements AfterViewInit, OnInit { buildChapterDownloadLink(download: Download, chapterFilename: string) { let baseDir = this.downloads.configuration["PUBLIC_HOST_URL"]; - if (download.quality == 'audio' || chapterFilename.endsWith('.mp3')) { + if (download.download_type === 'audio' || chapterFilename.endsWith('.mp3')) { baseDir = this.downloads.configuration["PUBLIC_HOST_AUDIO_URL"]; } @@ -655,10 +809,10 @@ export class App implements AfterViewInit, OnInit { } const url = urls[index]; this.batchImportStatus = `Importing URL ${index + 1} of ${urls.length}: ${url}`; - // Now pass the selected quality, format, folder, etc. to the add() method - this.downloads.add(url, this.quality, this.format, this.folder, this.customNamePrefix, + // Pass current selection options to backend + this.downloads.add(url, this.downloadType, this.codec, this.quality, this.format, this.folder, this.customNamePrefix, this.playlistItemLimit, this.autoStart, this.splitByChapters, this.chapterTemplate, - this.subtitleFormat, this.subtitleLanguage, this.subtitleMode, this.videoCodec) + this.subtitleLanguage, this.subtitleMode) .subscribe({ next: (status: Status) => { if (status.status === 'error') { @@ -891,7 +1045,7 @@ export class App implements AfterViewInit, OnInit { private refreshCookieStatus() { this.downloads.getCookieStatus().subscribe(data => { - this.hasCookies = data?.has_cookies || false; + this.hasCookies = !!(data && typeof data === 'object' && 'has_cookies' in data && data.has_cookies); }); } diff --git a/ui/src/app/interfaces/download.ts b/ui/src/app/interfaces/download.ts index 6d69785..c988f71 100644 --- a/ui/src/app/interfaces/download.ts +++ b/ui/src/app/interfaces/download.ts @@ -3,6 +3,8 @@ export interface Download { id: string; title: string; url: string; + download_type: string; + codec?: string; quality: string; format: string; folder: string; @@ -10,10 +12,8 @@ export interface Download { playlist_item_limit: number; split_by_chapters?: boolean; chapter_template?: string; - subtitle_format?: string; subtitle_language?: string; subtitle_mode?: string; - video_codec?: string; status: string; msg: string; percent: number; @@ -25,5 +25,5 @@ export interface Download { size?: number; error?: string; deleting?: boolean; - chapter_files?: Array<{ filename: string, size: number }>; + chapter_files?: { filename: string, size: number }[]; } diff --git a/ui/src/app/interfaces/formats.ts b/ui/src/app/interfaces/formats.ts index 0ea73af..9b1b100 100644 --- a/ui/src/app/interfaces/formats.ts +++ b/ui/src/app/interfaces/formats.ts @@ -1,81 +1,77 @@ -import { Format } from "./format"; +import { Quality } from "./quality"; +export interface Option { + id: string; + text: string; +} -export const Formats: Format[] = [ - { - id: 'any', - text: 'Any', - qualities: [ - { id: 'best', text: 'Best' }, - { id: '2160', text: '2160p' }, - { id: '1440', text: '1440p' }, - { id: '1080', text: '1080p' }, - { id: '720', text: '720p' }, - { id: '480', text: '480p' }, - { id: '360', text: '360p' }, - { id: '240', text: '240p' }, - { id: 'worst', text: 'Worst' }, - { id: 'audio', text: 'Audio Only' }, - ], - }, - { - id: 'mp4', - text: 'MP4', - qualities: [ - { id: 'best', text: 'Best' }, - { id: 'best_ios', text: 'Best (iOS)' }, - { id: '2160', text: '2160p' }, - { id: '1440', text: '1440p' }, - { id: '1080', text: '1080p' }, - { id: '720', text: '720p' }, - { id: '480', text: '480p' }, - { id: '360', text: '360p' }, - { id: '240', text: '240p' }, - { id: 'worst', text: 'Worst' }, - ], - }, - { - id: 'm4a', - text: 'M4A', - qualities: [ - { id: 'best', text: 'Best' }, - { id: '192', text: '192 kbps' }, - { id: '128', text: '128 kbps' }, - ], - }, - { - id: 'mp3', - text: 'MP3', - qualities: [ - { id: 'best', text: 'Best' }, - { id: '320', text: '320 kbps' }, - { id: '192', text: '192 kbps' }, - { id: '128', text: '128 kbps' }, - ], - }, - { - id: 'opus', - text: 'OPUS', - qualities: [{ id: 'best', text: 'Best' }], - }, - { - id: 'wav', - text: 'WAV', - qualities: [{ id: 'best', text: 'Best' }], - }, - { - id: 'flac', - text: 'FLAC', - qualities: [{ id: 'best', text: 'Best' }], - }, - { - id: 'thumbnail', - text: 'Thumbnail', - qualities: [{ id: 'best', text: 'Best' }], - }, - { - id: 'captions', - text: 'Captions', - qualities: [{ id: 'best', text: 'Best' }], - }, +export interface AudioFormatOption extends Option { + qualities: Quality[]; +} + +export const DOWNLOAD_TYPES: Option[] = [ + { id: "video", text: "Video" }, + { id: "audio", text: "Audio" }, + { id: "captions", text: "Captions" }, + { id: "thumbnail", text: "Thumbnail" }, ]; + +export const VIDEO_CODECS: Option[] = [ + { id: "auto", text: "Auto" }, + { id: "h264", text: "H.264" }, + { id: "h265", text: "H.265 (HEVC)" }, + { id: "av1", text: "AV1" }, + { id: "vp9", text: "VP9" }, +]; + +export const VIDEO_FORMATS: Option[] = [ + { id: "any", text: "Auto" }, + { id: "mp4", text: "MP4" }, + { id: "ios", text: "iOS Compatible" }, +]; + +export const VIDEO_QUALITIES: Quality[] = [ + { id: "best", text: "Best" }, + { id: "2160", text: "2160p" }, + { id: "1440", text: "1440p" }, + { id: "1080", text: "1080p" }, + { id: "720", text: "720p" }, + { id: "480", text: "480p" }, + { id: "360", text: "360p" }, + { id: "240", text: "240p" }, + { id: "worst", text: "Worst" }, +]; + +export const AUDIO_FORMATS: AudioFormatOption[] = [ + { + id: "m4a", + text: "M4A", + qualities: [ + { id: "best", text: "Best" }, + { id: "192", text: "192 kbps" }, + { id: "128", text: "128 kbps" }, + ], + }, + { + id: "mp3", + text: "MP3", + qualities: [ + { id: "best", text: "Best" }, + { id: "320", text: "320 kbps" }, + { id: "192", text: "192 kbps" }, + { id: "128", text: "128 kbps" }, + ], + }, + { id: "opus", text: "OPUS", qualities: [{ id: "best", text: "Best" }] }, + { id: "wav", text: "WAV", qualities: [{ id: "best", text: "Best" }] }, + { id: "flac", text: "FLAC", qualities: [{ id: "best", text: "Best" }] }, +]; + +export const CAPTION_FORMATS: Option[] = [ + { id: "srt", text: "SRT" }, + { id: "txt", text: "TXT (Text only)" }, + { id: "vtt", text: "VTT" }, + { id: "ttml", text: "TTML" }, +]; + +export const THUMBNAIL_FORMATS: Option[] = [{ id: "jpg", text: "JPG" }]; diff --git a/ui/src/app/services/downloads.service.ts b/ui/src/app/services/downloads.service.ts index 94d629d..6bbd0be 100644 --- a/ui/src/app/services/downloads.service.ts +++ b/ui/src/app/services/downloads.service.ts @@ -109,6 +109,8 @@ export class DownloadsService { public add( url: string, + downloadType: string, + codec: string, quality: string, format: string, folder: string, @@ -117,13 +119,13 @@ export class DownloadsService { autoStart: boolean, splitByChapters: boolean, chapterTemplate: string, - subtitleFormat: string, subtitleLanguage: string, subtitleMode: string, - videoCodec: string, ) { return this.http.post('add', { url: url, + download_type: downloadType, + codec: codec, quality: quality, format: format, folder: folder, @@ -132,10 +134,8 @@ export class DownloadsService { auto_start: autoStart, split_by_chapters: splitByChapters, chapter_template: chapterTemplate, - subtitle_format: subtitleFormat, subtitle_language: subtitleLanguage, subtitle_mode: subtitleMode, - video_codec: videoCodec, }).pipe( catchError(this.handleHTTPError) ); @@ -174,6 +174,8 @@ export class DownloadsService { status: string; msg?: string; }> { + const defaultDownloadType = 'video'; + const defaultCodec = 'auto'; const defaultQuality = 'best'; const defaultFormat = 'mp4'; const defaultFolder = ''; @@ -182,14 +184,14 @@ export class DownloadsService { const defaultAutoStart = true; const defaultSplitByChapters = false; const defaultChapterTemplate = this.configuration['OUTPUT_TEMPLATE_CHAPTER']; - const defaultSubtitleFormat = 'srt'; const defaultSubtitleLanguage = 'en'; const defaultSubtitleMode = 'prefer_manual'; - const defaultVideoCodec = 'auto'; return new Promise((resolve, reject) => { this.add( url, + defaultDownloadType, + defaultCodec, defaultQuality, defaultFormat, defaultFolder, @@ -198,10 +200,8 @@ export class DownloadsService { defaultAutoStart, defaultSplitByChapters, defaultChapterTemplate, - defaultSubtitleFormat, defaultSubtitleLanguage, defaultSubtitleMode, - defaultVideoCodec, ) .subscribe({ next: (response) => resolve(response), @@ -221,19 +221,19 @@ export class DownloadsService { uploadCookies(file: File) { const formData = new FormData(); formData.append('cookies', file); - return this.http.post('upload-cookies', formData).pipe( + return this.http.post<{ status: string; msg?: string }>('upload-cookies', formData).pipe( catchError(this.handleHTTPError) ); } deleteCookies() { - return this.http.post('delete-cookies', {}).pipe( + return this.http.post<{ status: string; msg?: string }>('delete-cookies', {}).pipe( catchError(this.handleHTTPError) ); } getCookieStatus() { - return this.http.get('cookie-status').pipe( + return this.http.get<{ status: string; has_cookies: boolean }>('cookie-status').pipe( catchError(this.handleHTTPError) ); } From 475aeb91bfafc79d66dd68ee74ae3acc3888bcd7 Mon Sep 17 00:00:00 2001 From: Alex Shnitman Date: Fri, 13 Mar 2026 19:49:18 +0200 Subject: [PATCH 010/107] add status indicator when adding a URL --- ui/src/app/app.html | 10 +++++++++- ui/src/app/app.sass | 7 +++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/ui/src/app/app.html b/ui/src/app/app.html index 9c3b8d7..a54dddb 100644 --- a/ui/src/app/app.html +++ b/ui/src/app/app.html @@ -104,7 +104,15 @@ Canceling... } @else if (addInProgress) { - + } @else { diff --git a/ui/src/app/app.sass b/ui/src/app/app.sass index e1d7e31..4ba26b8 100644 --- a/ui/src/app/app.sass +++ b/ui/src/app/app.sass @@ -112,6 +112,13 @@ td .spinner-border margin-right: 0.5rem +.add-progress-btn + min-width: 9.5rem + cursor: default + +.add-cancel-btn + min-width: 3.25rem + ::ng-deep .ng-select flex: 1 .ng-select-container From 8b0d682b359514f50789d930e41fe6322013e4a9 Mon Sep 17 00:00:00 2001 From: AutoUpdater Date: Sat, 14 Mar 2026 00:13:08 +0000 Subject: [PATCH 011/107] upgrade yt-dlp from 2026.3.3 to 2026.3.13 --- uv.lock | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/uv.lock b/uv.lock index 4f7ab85..080262a 100644 --- a/uv.lock +++ b/uv.lock @@ -167,6 +167,11 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/84/85/57c314a6b35336efbbdc13e5fc9ae13f6b60a0647cfa7c1221178ac6d8ae/brotlicffi-1.2.0.0.tar.gz", hash = "sha256:34345d8d1f9d534fcac2249e57a4c3c8801a33c9942ff9f8574f67a175e17adb", size = 476682, upload-time = "2025-11-21T18:17:57.334Z" } wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/87/ba6298c3d7f8d66ce80d7a487f2a487ebae74a79c6049c7c2990178ce529/brotlicffi-1.2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b13fb476a96f02e477a506423cb5e7bc21e0e3ac4c060c20ba31c44056e38c68", size = 433038, upload-time = "2026-03-05T17:57:37.96Z" }, + { url = "https://files.pythonhosted.org/packages/00/49/16c7a77d1cae0519953ef0389a11a9c2e2e62e87d04f8e7afbae40124255/brotlicffi-1.2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17db36fb581f7b951635cd6849553a95c6f2f53c1a707817d06eae5aeff5f6af", size = 1541124, upload-time = "2026-03-05T17:57:39.488Z" }, + { url = "https://files.pythonhosted.org/packages/e8/17/fab2c36ea820e2288f8c1bf562de1b6cd9f30e28d66f1ce2929a4baff6de/brotlicffi-1.2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40190192790489a7b054312163d0ce82b07d1b6e706251036898ce1684ef12e9", size = 1541983, upload-time = "2026-03-05T17:57:41.061Z" }, + { url = "https://files.pythonhosted.org/packages/78/c9/849a669b3b3bb8ac96005cdef04df4db658c33443a7fc704a6d4a2f07a56/brotlicffi-1.2.0.0-cp314-cp314t-win32.whl", hash = "sha256:a8079e8ecc32ecef728036a1d9b7105991ce6a5385cf51ee8c02297c90fb08c2", size = 349046, upload-time = "2026-03-05T17:57:42.76Z" }, + { url = "https://files.pythonhosted.org/packages/a4/25/09c0fd21cfc451fa38ad538f4d18d8be566746531f7f27143f63f8c45a9f/brotlicffi-1.2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ca90c4266704ca0a94de8f101b4ec029624273380574e4cf19301acfa46c61a0", size = 385653, upload-time = "2026-03-05T17:57:44.224Z" }, { url = "https://files.pythonhosted.org/packages/e4/df/a72b284d8c7bef0ed5756b41c2eb7d0219a1dd6ac6762f1c7bdbc31ef3af/brotlicffi-1.2.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:9458d08a7ccde8e3c0afedbf2c70a8263227a68dea5ab13590593f4c0a4fd5f4", size = 432340, upload-time = "2025-11-21T18:17:42.277Z" }, { url = "https://files.pythonhosted.org/packages/74/2b/cc55a2d1d6fb4f5d458fba44a3d3f91fb4320aa14145799fd3a996af0686/brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84e3d0020cf1bd8b8131f4a07819edee9f283721566fe044a20ec792ca8fd8b7", size = 1534002, upload-time = "2025-11-21T18:17:43.746Z" }, { url = "https://files.pythonhosted.org/packages/e4/9c/d51486bf366fc7d6735f0e46b5b96ca58dc005b250263525a1eea3cd5d21/brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:33cfb408d0cff64cd50bef268c0fed397c46fbb53944aa37264148614a62e990", size = 1536547, upload-time = "2025-11-21T18:17:45.729Z" }, @@ -951,11 +956,11 @@ wheels = [ [[package]] name = "yt-dlp" -version = "2026.3.3" +version = "2026.3.13" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/6f/7427d23609353e5ef3470ff43ef551b8bd7b166dd4fef48957f0d0e040fe/yt_dlp-2026.3.3.tar.gz", hash = "sha256:3db7969e3a8964dc786bdebcffa2653f31123bf2a630f04a17bdafb7bbd39952", size = 3118658, upload-time = "2026-03-03T16:54:53.909Z" } +sdist = { url = "https://files.pythonhosted.org/packages/34/69/59253e5627f583939e742a592f56dc7d7f30d164473e58f055e1fccdc02b/yt_dlp-2026.3.13.tar.gz", hash = "sha256:fb43659db684a3db6ff2f5c92e0f1641262f6ecc71dbb64fefe84177aaba9e36", size = 3117911, upload-time = "2026-03-13T09:02:22.711Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/a4/8b5cd28ab87aef48ef15e74241befec3445496327db028f34147a9e0f14f/yt_dlp-2026.3.3-py3-none-any.whl", hash = "sha256:166c6e68c49ba526474bd400e0129f58aa522c2896204aa73be669c3d2f15e63", size = 3315599, upload-time = "2026-03-03T16:54:51.899Z" }, + { url = "https://files.pythonhosted.org/packages/f0/ef/52ed7ed10d2e1a22badf74b520b617c48b0a725a981620393245ac842bf9/yt_dlp-2026.3.13-py3-none-any.whl", hash = "sha256:e22e7716f94c08e76b29c0172a3fe0c01d8cabab9bce7f528ad440d70a0d213c", size = 3315062, upload-time = "2026-03-13T09:02:20.357Z" }, ] [package.optional-dependencies] @@ -979,9 +984,9 @@ deno = [ [[package]] name = "yt-dlp-ejs" -version = "0.5.0" +version = "0.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/6b/0d/b9e4ab1b47cdeba0842df634b74b3c0144307640ad5b632a5e189c4ab7ce/yt_dlp_ejs-0.5.0.tar.gz", hash = "sha256:8dfae59e418232f485253dcf8e197fefa232423c3af7824fe19e4517b173293b", size = 98925, upload-time = "2026-02-21T19:29:16.844Z" } +sdist = { url = "https://files.pythonhosted.org/packages/63/39/57bc2dedbcd4c921fa740fc99f83ada045a219a0e9bb3283b9ab2102e840/yt_dlp_ejs-0.7.0.tar.gz", hash = "sha256:ecac13eb9ff948da84b39f1030fa03422abaf32dc58a0edd78f5dbcc03843556", size = 95961, upload-time = "2026-03-13T07:34:43.612Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/5b/1283356b70d4893a8a050cee15092e1b08ea15310b94365f88067146721b/yt_dlp_ejs-0.5.0-py3-none-any.whl", hash = "sha256:674fc0efea741d3100cdf3f0f9e123150715ee41edf47ea7a62fbdeda204bdec", size = 54032, upload-time = "2026-02-21T19:29:15.408Z" }, + { url = "https://files.pythonhosted.org/packages/59/f6/54fe93b9db02b7727043fb48816504f09066ca6d7f7d6145cd9d713a1047/yt_dlp_ejs-0.7.0-py3-none-any.whl", hash = "sha256:967e9cbe114ddfd046ff4668af18b1827b4597e2e47a83deea668a355828c798", size = 53444, upload-time = "2026-03-13T07:34:42.195Z" }, ] From 04959a61893ca38d036d43accb9ba4a09adf41ee Mon Sep 17 00:00:00 2001 From: Alex Shnitman Date: Sat, 14 Mar 2026 12:05:04 +0200 Subject: [PATCH 012/107] upgrade dependencies --- Dockerfile | 2 +- ui/package.json | 30 +- ui/pnpm-lock.yaml | 997 +++++++++++++++++++++++----------------------- uv.lock | 114 +++--- 4 files changed, 572 insertions(+), 571 deletions(-) diff --git a/Dockerfile b/Dockerfile index 3772784..4dff7c0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -34,7 +34,7 @@ RUN sed -i 's/\r$//g' docker-entrypoint.sh && \ UV_PROJECT_ENVIRONMENT=/usr/local uv sync --frozen --no-dev --compile-bytecode && \ uv cache clean && \ rm -f /usr/local/bin/uv /usr/local/bin/uvx /usr/local/bin/uvw && \ - curl -fsSL https://deno.land/install.sh | DENO_INSTALL=/usr/local sh -s -- -y v2.7.2 && \ + curl -fsSL https://deno.land/install.sh | DENO_INSTALL=/usr/local sh -s -- -y && \ apt-get purge -y --auto-remove build-essential && \ rm -rf /var/lib/apt/lists/* && \ mkdir /.cache && chmod 777 /.cache diff --git a/ui/package.json b/ui/package.json index 280616f..6729206 100644 --- a/ui/package.json +++ b/ui/package.json @@ -23,14 +23,14 @@ }, "private": true, "dependencies": { - "@angular/animations": "^21.2.1", - "@angular/common": "^21.2.1", - "@angular/compiler": "^21.2.1", - "@angular/core": "^21.2.1", - "@angular/forms": "^21.2.1", - "@angular/platform-browser": "^21.2.1", - "@angular/platform-browser-dynamic": "^21.2.1", - "@angular/service-worker": "^21.2.1", + "@angular/animations": "^21.2.4", + "@angular/common": "^21.2.4", + "@angular/compiler": "^21.2.4", + "@angular/core": "^21.2.4", + "@angular/forms": "^21.2.4", + "@angular/platform-browser": "^21.2.4", + "@angular/platform-browser-dynamic": "^21.2.4", + "@angular/service-worker": "^21.2.4", "@fortawesome/angular-fontawesome": "~4.0.0", "@fortawesome/fontawesome-svg-core": "^7.2.0", "@fortawesome/free-brands-svg-icons": "^7.2.0", @@ -48,16 +48,16 @@ }, "devDependencies": { "@angular-eslint/builder": "21.1.0", - "@angular/build": "^21.2.1", - "@angular/cli": "^21.2.1", - "@angular/compiler-cli": "^21.2.1", - "@angular/localize": "^21.2.1", - "@eslint/js": "^9.39.3", + "@angular/build": "^21.2.2", + "@angular/cli": "^21.2.2", + "@angular/compiler-cli": "^21.2.4", + "@angular/localize": "^21.2.4", + "@eslint/js": "^9.39.4", "angular-eslint": "21.1.0", - "eslint": "^9.39.3", + "eslint": "^9.39.4", "jsdom": "^27.4.0", "typescript": "~5.9.3", "typescript-eslint": "8.47.0", - "vitest": "^4.0.18" + "vitest": "^4.1.0" } } diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml index 0f8c51b..d43141e 100644 --- a/ui/pnpm-lock.yaml +++ b/ui/pnpm-lock.yaml @@ -9,32 +9,32 @@ importers: .: dependencies: '@angular/animations': - specifier: ^21.2.1 - version: 21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)) + specifier: ^21.2.4 + version: 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)) '@angular/common': - specifier: ^21.2.1 - version: 21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) + specifier: ^21.2.4 + version: 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) '@angular/compiler': - specifier: ^21.2.1 - version: 21.2.1 + specifier: ^21.2.4 + version: 21.2.4 '@angular/core': - specifier: ^21.2.1 - version: 21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0) + specifier: ^21.2.4 + version: 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0) '@angular/forms': - specifier: ^21.2.1 - version: 21.2.1(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.1(@angular/animations@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) + specifier: ^21.2.4 + version: 21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) '@angular/platform-browser': - specifier: ^21.2.1 - version: 21.2.1(@angular/animations@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)) + specifier: ^21.2.4 + version: 21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)) '@angular/platform-browser-dynamic': - specifier: ^21.2.1 - version: 21.2.1(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/compiler@21.2.1)(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.1(@angular/animations@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))) + specifier: ^21.2.4 + version: 21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))) '@angular/service-worker': - specifier: ^21.2.1 - version: 21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) + specifier: ^21.2.4 + version: 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) '@fortawesome/angular-fontawesome': specifier: ~4.0.0 - version: 4.0.0(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)) + version: 4.0.0(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)) '@fortawesome/fontawesome-svg-core': specifier: ^7.2.0 version: 7.2.0 @@ -49,10 +49,10 @@ importers: version: 7.2.0 '@ng-bootstrap/ng-bootstrap': specifier: ^20.0.0 - version: 20.0.0(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/forms@21.2.1(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.1(@angular/animations@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2))(@angular/localize@21.2.1(@angular/compiler-cli@21.2.1(@angular/compiler@21.2.1)(typescript@5.9.3))(@angular/compiler@21.2.1))(@popperjs/core@2.11.8)(rxjs@7.8.2) + version: 20.0.0(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/forms@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2))(@angular/localize@21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4))(@popperjs/core@2.11.8)(rxjs@7.8.2) '@ng-select/ng-select': specifier: ^21.5.2 - version: 21.5.2(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/forms@21.2.1(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.1(@angular/animations@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)) + version: 21.5.2(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/forms@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)) '@popperjs/core': specifier: ^2.11.8 version: 2.11.8 @@ -61,10 +61,10 @@ importers: version: 5.3.8(@popperjs/core@2.11.8) ngx-cookie-service: specifier: ^21.1.0 - version: 21.1.0(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)) + version: 21.1.0(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)) ngx-socket-io: specifier: ~4.10.0 - version: 4.10.0(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) + version: 4.10.0(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) rxjs: specifier: ~7.8.2 version: 7.8.2 @@ -77,28 +77,28 @@ importers: devDependencies: '@angular-eslint/builder': specifier: 21.1.0 - version: 21.1.0(@angular/cli@21.2.1(@types/node@25.3.5)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@9.39.3)(typescript@5.9.3) + version: 21.1.0(@angular/cli@21.2.2(@types/node@25.5.0)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@9.39.4)(typescript@5.9.3) '@angular/build': - specifier: ^21.2.1 - version: 21.2.1(@angular/compiler-cli@21.2.1(@angular/compiler@21.2.1)(typescript@5.9.3))(@angular/compiler@21.2.1)(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/localize@21.2.1(@angular/compiler-cli@21.2.1(@angular/compiler@21.2.1)(typescript@5.9.3))(@angular/compiler@21.2.1))(@angular/platform-browser@21.2.1(@angular/animations@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/service-worker@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@types/node@25.3.5)(chokidar@5.0.0)(postcss@8.5.8)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.5)(jsdom@27.4.0)(sass@1.97.3)) + specifier: ^21.2.2 + version: 21.2.2(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/localize@21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4))(@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/service-worker@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@types/node@25.5.0)(chokidar@5.0.0)(postcss@8.5.8)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.1.0(@types/node@25.5.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@25.5.0)(sass@1.97.3))) '@angular/cli': - specifier: ^21.2.1 - version: 21.2.1(@types/node@25.3.5)(chokidar@5.0.0) + specifier: ^21.2.2 + version: 21.2.2(@types/node@25.5.0)(chokidar@5.0.0) '@angular/compiler-cli': - specifier: ^21.2.1 - version: 21.2.1(@angular/compiler@21.2.1)(typescript@5.9.3) + specifier: ^21.2.4 + version: 21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3) '@angular/localize': - specifier: ^21.2.1 - version: 21.2.1(@angular/compiler-cli@21.2.1(@angular/compiler@21.2.1)(typescript@5.9.3))(@angular/compiler@21.2.1) + specifier: ^21.2.4 + version: 21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4) '@eslint/js': - specifier: ^9.39.3 - version: 9.39.3 + specifier: ^9.39.4 + version: 9.39.4 angular-eslint: specifier: 21.1.0 - version: 21.1.0(@angular/cli@21.2.1(@types/node@25.3.5)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@9.39.3)(typescript-eslint@8.47.0(eslint@9.39.3)(typescript@5.9.3))(typescript@5.9.3) + version: 21.1.0(@angular/cli@21.2.2(@types/node@25.5.0)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@9.39.4)(typescript-eslint@8.47.0(eslint@9.39.4)(typescript@5.9.3))(typescript@5.9.3) eslint: - specifier: ^9.39.3 - version: 9.39.3 + specifier: ^9.39.4 + version: 9.39.4 jsdom: specifier: ^27.4.0 version: 27.4.0 @@ -107,10 +107,10 @@ importers: version: 5.9.3 typescript-eslint: specifier: 8.47.0 - version: 8.47.0(eslint@9.39.3)(typescript@5.9.3) + version: 8.47.0(eslint@9.39.4)(typescript@5.9.3) vitest: - specifier: ^4.0.18 - version: 4.0.18(@types/node@25.3.5)(jsdom@27.4.0)(sass@1.97.3) + specifier: ^4.1.0 + version: 4.1.0(@types/node@25.5.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@25.5.0)(sass@1.97.3)) packages: @@ -177,13 +177,13 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - '@angular-devkit/architect@0.2102.1': - resolution: {integrity: sha512-x2Qqz6oLYvEh9UBUG0AP1A4zROO/VP+k+zM9+4c2uZw1uqoBQFmutqgzncjVU7cR9R0RApgx9JRZHDFtQru68w==} + '@angular-devkit/architect@0.2102.2': + resolution: {integrity: sha512-CDvFtXwyBtMRkTQnm+LfBNLL0yLV8ZGskrM1T6VkcGwXGFDott1FxUdj96ViodYsYL5fbJr0MNA6TlLcanV3kQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} hasBin: true - '@angular-devkit/core@21.2.1': - resolution: {integrity: sha512-TpXGjERqVPN8EPt7LdmWAwh0oNQ/6uWFutzGZiXhJy81n1zb1O1XrqhRAmvP1cAo5O+na6IV2JkkCmxL6F8GUg==} + '@angular-devkit/core@21.2.2': + resolution: {integrity: sha512-xUeKGe4BDQpkz0E6fnAPIJXE0y0nqtap0KhJIBhvN7xi3NenIzTmoi6T9Yv5OOBUdLZbOm4SOel8MhdXiIBpAQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} peerDependencies: chokidar: ^5.0.0 @@ -191,8 +191,8 @@ packages: chokidar: optional: true - '@angular-devkit/schematics@21.2.1': - resolution: {integrity: sha512-CWoamHaasAHMjHcYqxbj0tMnoXxdGotcAz2SpiuWtH28Lnf5xfbTaJn/lwdMP8Wdh4tgA+uYh2l45A5auCwmkw==} + '@angular-devkit/schematics@21.2.2': + resolution: {integrity: sha512-CCeyQxGUq+oyGnHd7PfcYIVbj9pRnqjQq0rAojoAqs1BJdtInx9weLBCLy+AjM3NHePeZrnwm+wEVr8apED8kg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} '@angular-eslint/builder@21.1.0': @@ -239,14 +239,14 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '*' - '@angular/animations@21.2.1': - resolution: {integrity: sha512-zT/S29pUTbziCLvZ2itBdNWd5i8tsXexofH7KA4n2yvYmK1EhNpE7TlHRjghmsHgtDt4VnGiMW4zXEyrl05Dwg==} + '@angular/animations@21.2.4': + resolution: {integrity: sha512-hO1P7ks9n7lW3D31bzHohSuoAaj05xJUlK8rZgX8IkH5DLx4qhvfNh0t4bbLuBJLP2r1TaLsQ8KFcemCkFRO2w==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@angular/core': 21.2.1 + '@angular/core': 21.2.4 - '@angular/build@21.2.1': - resolution: {integrity: sha512-cUpLNHJp9taII/FOcJHHfQYlMcZSRaf6eIxgSNS6Xfx1CeGoJNDN+J8+GFk+H1CPJt1EvbfyZ+dE5DbsgTD/QQ==} + '@angular/build@21.2.2': + resolution: {integrity: sha512-Vq2eIneNxzhHm1MwEmRqEJDwHU9ODfSRDaMWwtysGMhpoMQmLdfTqkQDmkC2qVUr8mV8Z1i5I+oe5ZJaMr/PlQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} peerDependencies: '@angular/compiler': ^21.0.0 @@ -256,7 +256,7 @@ packages: '@angular/platform-browser': ^21.0.0 '@angular/platform-server': ^21.0.0 '@angular/service-worker': ^21.0.0 - '@angular/ssr': ^21.2.1 + '@angular/ssr': ^21.2.2 karma: ^6.4.0 less: ^4.2.0 ng-packagr: ^21.0.0 @@ -291,38 +291,38 @@ packages: vitest: optional: true - '@angular/cli@21.2.1': - resolution: {integrity: sha512-5SRfMTgwFj1zXOpfeZWHsxZBni0J4Xz7/CbewG47D6DmbstOrSdgt6eNzJ62R650t0G9dpri2YvToZgImtbjOQ==} + '@angular/cli@21.2.2': + resolution: {integrity: sha512-eZo8/qX+ZIpIWc0CN+cCX13Lbgi/031wAp8DRVhDDO6SMVtcr/ObOQ2S16+pQdOMXxiG3vby6IhzJuz9WACzMQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} hasBin: true - '@angular/common@21.2.1': - resolution: {integrity: sha512-xhv2i1Q9s1kpGbGsfj+o36+XUC/TQLcZyRuRxn3GwaN7Rv34FabC88ycpvoE+sW/txj4JRx9yPA0dRSZjwZ+Gg==} + '@angular/common@21.2.4': + resolution: {integrity: sha512-NrP6qOuUpo3fqq14UJ1b2bIRtWsfvxh1qLqOyFV4gfBrHhXd0XffU1LUlUw1qp4w1uBSgPJ0/N5bSPUWrAguVg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@angular/core': 21.2.1 + '@angular/core': 21.2.4 rxjs: ^6.5.3 || ^7.4.0 - '@angular/compiler-cli@21.2.1': - resolution: {integrity: sha512-qYCWLGtEju4cDtYLi4ZzbwKoF0lcGs+Lc31kuESvAzYvWNgk2EUOtwWo8kbgpAzAwSYodtxW6Q90iWEwfU6elw==} + '@angular/compiler-cli@21.2.4': + resolution: {integrity: sha512-vGjd7DZo/Ox50pQCm5EycmBu91JclimPtZoyNXu/2hSxz3oAkzwiHCwlHwk2g58eheSSp+lYtYRLmHAqSVZLjg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} hasBin: true peerDependencies: - '@angular/compiler': 21.2.1 + '@angular/compiler': 21.2.4 typescript: '>=5.9 <6.1' peerDependenciesMeta: typescript: optional: true - '@angular/compiler@21.2.1': - resolution: {integrity: sha512-FxWaSaii1vfHIFA+JksqQ8NGB2frfqCrs7Ju50a44kbwR4fmanfn/VsiS/CbwBp9vcyT/Br9X/jAG4RuK/U2nw==} + '@angular/compiler@21.2.4': + resolution: {integrity: sha512-9+ulVK3idIo/Tu4X2ic7/V0+Uj7pqrOAbOuIirYe6Ymm3AjexuFRiGBbfcH0VJhQ5cf8TvIJ1fuh+MI4JiRIxA==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} - '@angular/core@21.2.1': - resolution: {integrity: sha512-pFTbg03s2ZI5cHNT+eWsGjwIIKiYkeAnodFbCAHjwFi9KCEYlTykFLjr9lcpGrBddfmAH7GE08Q73vgmsdcNHw==} + '@angular/core@21.2.4': + resolution: {integrity: sha512-2+gd67ZuXHpGOqeb2o7XZPueEWEP81eJza2tSHkT5QMV8lnYllDEmaNnkPxnIjSLGP1O3PmiXxo4z8ibHkLZwg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@angular/compiler': 21.2.1 + '@angular/compiler': 21.2.4 rxjs: ^6.5.3 || ^7.4.0 zone.js: ~0.15.0 || ~0.16.0 peerDependenciesMeta: @@ -331,49 +331,49 @@ packages: zone.js: optional: true - '@angular/forms@21.2.1': - resolution: {integrity: sha512-6aqOPk9xoa0dfeUDeEbhaiPhmt6MQrdn59qbGAomn9RMXA925TrHbJhSIkp9tXc2Fr4aJRi8zkD/cdXEc1IYeA==} + '@angular/forms@21.2.4': + resolution: {integrity: sha512-1fOhctA9ADEBYjI3nPQUR5dHsK2+UWAjup37Ksldk/k0w8UpD5YsN7JVNvsDMZRFMucKYcGykPblU7pABtsqnQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@angular/common': 21.2.1 - '@angular/core': 21.2.1 - '@angular/platform-browser': 21.2.1 + '@angular/common': 21.2.4 + '@angular/core': 21.2.4 + '@angular/platform-browser': 21.2.4 rxjs: ^6.5.3 || ^7.4.0 - '@angular/localize@21.2.1': - resolution: {integrity: sha512-2QsN33fLO3N/RRFfUxDKHMX/Y/2TH90Tx51Wi6hi1do9IJdlfEe1qBw+5F0g1F1CuFEYgZWMJdZIK7LPHpuDzw==} + '@angular/localize@21.2.4': + resolution: {integrity: sha512-brKKeH+jaTlY4coIOinKQtitLCguQzyniKYtfrhCvZSN0ap4W4PljAT5w3l+1a8e7/ThM1JVQpqtVCCcJHJZSg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} hasBin: true peerDependencies: - '@angular/compiler': 21.2.1 - '@angular/compiler-cli': 21.2.1 + '@angular/compiler': 21.2.4 + '@angular/compiler-cli': 21.2.4 - '@angular/platform-browser-dynamic@21.2.1': - resolution: {integrity: sha512-J4KnrXjgSuk7KjEm79/RK1yyzR867sIyT5mcG6jx2KmkjspFJd4OeOux7Oj7lSBM7+nDEsKC9F6s0x3dC0hCPQ==} + '@angular/platform-browser-dynamic@21.2.4': + resolution: {integrity: sha512-LRJLnGh4rdgD0+S5xuDd4YRm5bV8WP2e6F1Pe5rIr6N4V9ofgpB0/uOjYy9se99FJZjoyPnpxaKsp8+XA753Zg==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@angular/common': 21.2.1 - '@angular/compiler': 21.2.1 - '@angular/core': 21.2.1 - '@angular/platform-browser': 21.2.1 + '@angular/common': 21.2.4 + '@angular/compiler': 21.2.4 + '@angular/core': 21.2.4 + '@angular/platform-browser': 21.2.4 - '@angular/platform-browser@21.2.1': - resolution: {integrity: sha512-k4SJLxIaLT26vLjLuFL+ho0BiG5PrdxEsjsXFC7w5iUhomeouzkHVTZ4t7gaLNKrdRD7QNtU4Faw0nL0yx0ZPQ==} + '@angular/platform-browser@21.2.4': + resolution: {integrity: sha512-1A9e/cQVu+3BkRCktLcO3RZGuw8NOTHw1frUUrpAz+iMyvIT4sDRFbL+U1g8qmOCZqRNC1Pi1HZfZ1kl6kvrcQ==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} peerDependencies: - '@angular/animations': 21.2.1 - '@angular/common': 21.2.1 - '@angular/core': 21.2.1 + '@angular/animations': 21.2.4 + '@angular/common': 21.2.4 + '@angular/core': 21.2.4 peerDependenciesMeta: '@angular/animations': optional: true - '@angular/service-worker@21.2.1': - resolution: {integrity: sha512-mFyEVh5KazB6wr9uoXhlDQQDaicH9/t2m6lsN+/t2y6iMPpTIuTbWYHXX1uVbLKcxne54ei78NgD3wNS7DMfmg==} + '@angular/service-worker@21.2.4': + resolution: {integrity: sha512-YcPMb0co2hEDwzOG5S27b6f8rotXEUDx88nQuhHDl/ztuzXaxKklJ21qVDVZ0R433YBCRQJl2D6ZrpJojsnBFw==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0} hasBin: true peerDependencies: - '@angular/core': 21.2.1 + '@angular/core': 21.2.4 rxjs: ^6.5.3 || ^7.4.0 '@asamuzakjp/css-color@4.1.2': @@ -491,14 +491,14 @@ packages: resolution: {integrity: sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==} engines: {node: '>=20.19.0'} - '@emnapi/core@1.8.1': - resolution: {integrity: sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==} + '@emnapi/core@1.9.0': + resolution: {integrity: sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==} - '@emnapi/runtime@1.8.1': - resolution: {integrity: sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==} + '@emnapi/runtime@1.9.0': + resolution: {integrity: sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==} - '@emnapi/wasi-threads@1.1.0': - resolution: {integrity: sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==} + '@emnapi/wasi-threads@1.2.0': + resolution: {integrity: sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==} '@esbuild/aix-ppc64@0.27.3': resolution: {integrity: sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg==} @@ -666,8 +666,8 @@ packages: resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@eslint/config-array@0.21.1': - resolution: {integrity: sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==} + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/config-helpers@0.4.2': @@ -678,12 +678,12 @@ packages: resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/eslintrc@3.3.4': - resolution: {integrity: sha512-4h4MVF8pmBsncB60r0wSJiIeUKTSD4m7FmTFThG8RHlsg9ajqckLm9OraguFGZE4vVdpiI1Q4+hFnisopmG6gQ==} + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@eslint/js@9.39.3': - resolution: {integrity: sha512-1B1VkCq6FuUNlQvlBYb+1jDu/gV297TIs/OeiaSR9l1H27SVW55ONE1e1Vp16NqP683+xEGzxYtv4XCiDPaQiw==} + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.7': @@ -1496,8 +1496,8 @@ packages: cpu: [x64] os: [win32] - '@schematics/angular@21.2.1': - resolution: {integrity: sha512-DjrHRMoILhbZ6tc7aNZWuHA1wCm1iU/JN1TxAwNEyIBgyU3Fx8Z5baK4w0TCpOIPt0RLWVgP2L7kka9aXWCUFA==} + '@schematics/angular@21.2.2': + resolution: {integrity: sha512-Ywa6HDtX7TRBQZTVMMnxX3Mk7yVnG8KtSFaXWrkx779+q8tqYdBwNwAqbNd4Zatr1GccKaz9xcptHJta5+DTxw==} engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'} '@sigstore/bundle@4.0.0': @@ -1571,8 +1571,11 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/node@25.3.5': - resolution: {integrity: sha512-oX8xrhvpiyRCQkG1MFchB09f+cXftgIXb3a7UUa4Y3wpmZPw5tyZGTLWhlESOLq1Rq6oDlc8npVU2/9xiCuXMA==} + '@types/node@25.5.0': + resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} + + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} '@typescript-eslint/eslint-plugin@8.47.0': resolution: {integrity: sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==} @@ -1595,8 +1598,8 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/project-service@8.56.1': - resolution: {integrity: sha512-TAdqQTzHNNvlVFfR+hu2PDJrURiwKsUvxFn1M0h95BB8ah5jejas08jUWG4dBA68jDMI988IvtfdAI53JzEHOQ==} + '@typescript-eslint/project-service@8.57.0': + resolution: {integrity: sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' @@ -1605,8 +1608,8 @@ packages: resolution: {integrity: sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/scope-manager@8.56.1': - resolution: {integrity: sha512-YAi4VDKcIZp0O4tz/haYKhmIDZFEUPOreKbfdAN3SzUDMcPhJ8QI99xQXqX+HoUVq8cs85eRKnD+rne2UAnj2w==} + '@typescript-eslint/scope-manager@8.57.0': + resolution: {integrity: sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/tsconfig-utils@8.47.0': @@ -1615,8 +1618,8 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/tsconfig-utils@8.56.1': - resolution: {integrity: sha512-qOtCYzKEeyr3aR9f28mPJqBty7+DBqsdd63eO0yyDwc6vgThj2UjWfJIcsFeSucYydqcuudMOprZ+x1SpF3ZuQ==} + '@typescript-eslint/tsconfig-utils@8.57.0': + resolution: {integrity: sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' @@ -1632,8 +1635,8 @@ packages: resolution: {integrity: sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/types@8.56.1': - resolution: {integrity: sha512-dbMkdIUkIkchgGDIv7KLUpa0Mda4IYjo4IAMJUZ+3xNoUXxMsk9YtKpTHSChRS85o+H9ftm51gsK1dZReY9CVw==} + '@typescript-eslint/types@8.57.0': + resolution: {integrity: sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/typescript-estree@8.47.0': @@ -1642,8 +1645,8 @@ packages: peerDependencies: typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/typescript-estree@8.56.1': - resolution: {integrity: sha512-qzUL1qgalIvKWAf9C1HpvBjif+Vm6rcT5wZd4VoMb9+Km3iS3Cv9DY6dMRMDtPnwRAFyAi7YXJpTIEXLvdfPxg==} + '@typescript-eslint/typescript-estree@8.57.0': + resolution: {integrity: sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.0.0' @@ -1655,8 +1658,8 @@ packages: eslint: ^8.57.0 || ^9.0.0 typescript: '>=4.8.4 <6.0.0' - '@typescript-eslint/utils@8.56.1': - resolution: {integrity: sha512-HPAVNIME3tABJ61siYlHzSWCGtOoeP2RTIaHXFMPqjrQKCGB9OgUVdiNgH7TJS2JNIQ5qQ4RsAUDuGaGme/KOA==} + '@typescript-eslint/utils@8.57.0': + resolution: {integrity: sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 @@ -1666,8 +1669,8 @@ packages: resolution: {integrity: sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/visitor-keys@8.56.1': - resolution: {integrity: sha512-KiROIzYdEV85YygXw6BI/Dx4fnBlFQu6Mq4QE4MOH9fFnhohw6wX/OAvDY2/C+ut0I3RSPKenvZJIVYqJNkhEw==} + '@typescript-eslint/visitor-keys@8.57.0': + resolution: {integrity: sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@vitejs/plugin-basic-ssl@2.1.4': @@ -1676,34 +1679,34 @@ packages: peerDependencies: vite: ^6.0.0 || ^7.0.0 - '@vitest/expect@4.0.18': - resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + '@vitest/expect@4.1.0': + resolution: {integrity: sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==} - '@vitest/mocker@4.0.18': - resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} + '@vitest/mocker@4.1.0': + resolution: {integrity: sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==} peerDependencies: msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0-0 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 peerDependenciesMeta: msw: optional: true vite: optional: true - '@vitest/pretty-format@4.0.18': - resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + '@vitest/pretty-format@4.1.0': + resolution: {integrity: sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==} - '@vitest/runner@4.0.18': - resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + '@vitest/runner@4.1.0': + resolution: {integrity: sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==} - '@vitest/snapshot@4.0.18': - resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + '@vitest/snapshot@4.1.0': + resolution: {integrity: sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==} - '@vitest/spy@4.0.18': - resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + '@vitest/spy@4.1.0': + resolution: {integrity: sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==} - '@vitest/utils@4.0.18': - resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + '@vitest/utils@4.1.0': + resolution: {integrity: sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==} '@yarnpkg/lockfile@1.1.0': resolution: {integrity: sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==} @@ -1806,8 +1809,8 @@ packages: resolution: {integrity: sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==} engines: {node: ^4.5.0 || >= 5.9} - baseline-browser-mapping@2.10.0: - resolution: {integrity: sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==} + baseline-browser-mapping@2.10.7: + resolution: {integrity: sha512-1ghYO3HnxGec0TCGBXiDLVns4eCSx4zJpxnHrlqFQajmhfKMQBzUGDdkMK7fUW7PTHTeLf+j87aTuKuuwWzMGw==} engines: {node: '>=6.0.0'} hasBin: true @@ -1872,8 +1875,8 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - caniuse-lite@1.0.30001777: - resolution: {integrity: sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==} + caniuse-lite@1.0.30001778: + resolution: {integrity: sha512-PN7uxFL+ExFJO61aVmP1aIEG4i9whQd4eoSCebav62UwDyp5OHh06zN4jqKSMePVgxHifCw1QJxdRkA1Pisekg==} chai@6.2.2: resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} @@ -2030,8 +2033,8 @@ packages: ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} - electron-to-chromium@1.5.307: - resolution: {integrity: sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==} + electron-to-chromium@1.5.313: + resolution: {integrity: sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==} emoji-regex@10.6.0: resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} @@ -2050,8 +2053,8 @@ packages: resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} engines: {node: '>=10.0.0'} - engine.io@6.6.5: - resolution: {integrity: sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==} + engine.io@6.6.6: + resolution: {integrity: sha512-U2SN0w3OpjFRVlrc17E6TMDmH58Xl9rai1MblNjAdwWp07Kk+llmzX0hjDpQdrDGzwmvOtgM5yI+meYX6iZ2xA==} engines: {node: '>=10.2.0'} entities@4.5.0: @@ -2085,8 +2088,8 @@ packages: resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} engines: {node: '>= 0.4'} - es-module-lexer@1.7.0: - resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-module-lexer@2.0.0: + resolution: {integrity: sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==} es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} @@ -2112,8 +2115,8 @@ packages: resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint-scope@9.1.1: - resolution: {integrity: sha512-GaUN0sWim5qc8KVErfPBWmc31LEsOkrUJbvJZV+xuL3u2phMUK4HIvXlWAakfC8W4nzlK+chPEAkYOYb5ZScIw==} + eslint-scope@9.1.2: + resolution: {integrity: sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} eslint-visitor-keys@3.4.3: @@ -2128,8 +2131,8 @@ packages: resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - eslint@9.39.3: - resolution: {integrity: sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==} + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -2183,8 +2186,8 @@ packages: exponential-backoff@3.1.3: resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} - express-rate-limit@8.3.0: - resolution: {integrity: sha512-KJzBawY6fB9FiZGdE/0aftepZ91YlaGIrV8vgblRM3J8X+dHx/aiowJWwkx6LIGyuqGiANsjSwwrbb8mifOJ4Q==} + express-rate-limit@8.3.1: + resolution: {integrity: sha512-D1dKN+cmyPWuvB+G2SREQDzPY1agpBIcTa9sJxOPMCNeH3gwzhqJRDWCXW3gg0y//+LQ/8j52JbMROWyrKdMdw==} engines: {node: '>= 16'} peerDependencies: express: '>= 4.11' @@ -2241,8 +2244,8 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flatted@3.3.4: - resolution: {integrity: sha512-3+mMldrTAPdta5kjX2G2J7iX4zxtnwpdA8Tr2ZSjkyPSanvbZAcy6flmtnXbEybHrDcU9641lxrMfFuUxVz9vA==} + flatted@3.4.1: + resolution: {integrity: sha512-IxfVbRFVlV8V/yRaGzk0UVIcsKKHMSfYw66T/u4nTwlWteQePsxe//LjudR1AMX4tZW3WFCh3Zqa/sjlqpbURQ==} forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} @@ -2325,8 +2328,8 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} - hono@4.12.5: - resolution: {integrity: sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==} + hono@4.12.8: + resolution: {integrity: sha512-VJCEvtrezO1IAR+kqEYnxUOoStaQPGrCmX3j4wDTNOcD1uRPFpGlwQUIW8niPuvHXaTUxeOUl5MMDGrl+tmO9A==} engines: {node: '>=16.9.0'} hosted-git-info@9.0.2: @@ -2446,8 +2449,8 @@ packages: resolution: {integrity: sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==} engines: {node: '>=10'} - jose@6.2.0: - resolution: {integrity: sha512-xsfE1TcSCbUdo6U07tR0mvhg0flGxU8tPLbF03mirl2ukGQENhUg4ubGYQnhVH0b5stLlPM+WOqDkEl1R1y5sQ==} + jose@6.2.1: + resolution: {integrity: sha512-jUaKr1yrbfaImV7R2TN/b3IcZzsw38/chqMpo2XJ7i2F8AfM/lA4G1goC3JVEwg0H7UldTmSt3P68nt31W7/mw==} js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2531,8 +2534,8 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} - lru-cache@11.2.6: - resolution: {integrity: sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==} + lru-cache@11.2.7: + resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} engines: {node: 20 || >=22} lru-cache@5.1.1: @@ -2642,8 +2645,8 @@ packages: resolution: {integrity: sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==} hasBin: true - msgpackr@1.11.8: - resolution: {integrity: sha512-bC4UGzHhVvgDNS7kn9tV8fAucIYUBuGojcaLiz7v+P63Lmtm0Xeji8B/8tYKddALXxJLpwIeBmUN3u64C4YkRA==} + msgpackr@1.11.9: + resolution: {integrity: sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw==} mute-stream@2.0.0: resolution: {integrity: sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==} @@ -3084,8 +3087,8 @@ packages: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} - std-env@3.10.0: - resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + std-env@4.0.0: + resolution: {integrity: sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==} stdin-discarder@0.3.1: resolution: {integrity: sha512-reExS1kSGoElkextOcPkel4NE99S0BWxjUHQeDFnR8S993JxpPX7KU4MNmO19NXhlJp+8dmdCbKQVNgLJh2teA==} @@ -3122,30 +3125,30 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} - tar@7.5.10: - resolution: {integrity: sha512-8mOPs1//5q/rlkNSPcCegA6hiHJYDmSLEI8aMH/CdSQJNWztHC9WHNam5zdQlfpTwB9Xp7IBEsHfV5LKMJGVAw==} + tar@7.5.11: + resolution: {integrity: sha512-ChjMH33/KetonMTAtpYdgUFr0tbz69Fp2v7zWxQfYZX4g5ZN2nOBXm1R2xyA+lMIKrLKIoKAwFj93jE/avX9cQ==} engines: {node: '>=18'} tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} - tinyexec@1.0.2: - resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + tinyexec@1.0.4: + resolution: {integrity: sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==} engines: {node: '>=18'} tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} - tinyrainbow@3.0.3: - resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} engines: {node: '>=14.0.0'} - tldts-core@7.0.24: - resolution: {integrity: sha512-pj7yygNMoMRqG7ML2SDQ0xNIOfN3IBDUcPVM2Sg6hP96oFNN2nqnzHreT3z9xLq85IWJyNTvD38O002DdOrPMw==} + tldts-core@7.0.25: + resolution: {integrity: sha512-ZjCZK0rppSBu7rjHYDYsEaMOIbbT+nWF57hKkv4IUmZWBNrBWBOjIElc0mKRgLM8bm7x/BBlof6t2gi/Oq/Asw==} - tldts@7.0.24: - resolution: {integrity: sha512-1r6vQTTt1rUiJkI5vX7KG8PR342Ru/5Oh13kEQP2SMbRSZpOey9SrBe27IDxkoWulx8ShWu4K6C0BkctP8Z1bQ==} + tldts@7.0.25: + resolution: {integrity: sha512-keinCnPbwXEUG3ilrWQZU+CqcTTzHq9m2HhoUP2l7Xmi8l1LuijAXLpAJ5zRW+ifKTNscs4NdCkfkDCBYm352w==} hasBin: true to-regex-range@5.0.1: @@ -3156,8 +3159,8 @@ packages: resolution: {integrity: sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==} engines: {node: '>=0.6'} - tough-cookie@6.0.0: - resolution: {integrity: sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==} + tough-cookie@6.0.1: + resolution: {integrity: sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==} engines: {node: '>=16'} tr46@6.0.0: @@ -3273,20 +3276,21 @@ packages: yaml: optional: true - vitest@4.0.18: - resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} + vitest@4.1.0: + resolution: {integrity: sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==} 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.18 - '@vitest/browser-preview': 4.0.18 - '@vitest/browser-webdriverio': 4.0.18 - '@vitest/ui': 4.0.18 + '@vitest/browser-playwright': 4.1.0 + '@vitest/browser-preview': 4.1.0 + '@vitest/browser-webdriverio': 4.1.0 + '@vitest/ui': 4.1.0 happy-dom: '*' jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0-0 peerDependenciesMeta: '@edge-runtime/vm': optional: true @@ -3537,14 +3541,14 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 - '@angular-devkit/architect@0.2102.1(chokidar@5.0.0)': + '@angular-devkit/architect@0.2102.2(chokidar@5.0.0)': dependencies: - '@angular-devkit/core': 21.2.1(chokidar@5.0.0) + '@angular-devkit/core': 21.2.2(chokidar@5.0.0) rxjs: 7.8.2 transitivePeerDependencies: - chokidar - '@angular-devkit/core@21.2.1(chokidar@5.0.0)': + '@angular-devkit/core@21.2.2(chokidar@5.0.0)': dependencies: ajv: 8.18.0 ajv-formats: 3.0.1(ajv@8.18.0) @@ -3555,9 +3559,9 @@ snapshots: optionalDependencies: chokidar: 5.0.0 - '@angular-devkit/schematics@21.2.1(chokidar@5.0.0)': + '@angular-devkit/schematics@21.2.2(chokidar@5.0.0)': dependencies: - '@angular-devkit/core': 21.2.1(chokidar@5.0.0) + '@angular-devkit/core': 21.2.2(chokidar@5.0.0) jsonc-parser: 3.3.1 magic-string: 0.30.21 ora: 9.3.0 @@ -3565,46 +3569,46 @@ snapshots: transitivePeerDependencies: - chokidar - '@angular-eslint/builder@21.1.0(@angular/cli@21.2.1(@types/node@25.3.5)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@9.39.3)(typescript@5.9.3)': + '@angular-eslint/builder@21.1.0(@angular/cli@21.2.2(@types/node@25.5.0)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@9.39.4)(typescript@5.9.3)': dependencies: - '@angular-devkit/architect': 0.2102.1(chokidar@5.0.0) - '@angular-devkit/core': 21.2.1(chokidar@5.0.0) - '@angular/cli': 21.2.1(@types/node@25.3.5)(chokidar@5.0.0) - eslint: 9.39.3 + '@angular-devkit/architect': 0.2102.2(chokidar@5.0.0) + '@angular-devkit/core': 21.2.2(chokidar@5.0.0) + '@angular/cli': 21.2.2(@types/node@25.5.0)(chokidar@5.0.0) + eslint: 9.39.4 typescript: 5.9.3 transitivePeerDependencies: - chokidar '@angular-eslint/bundled-angular-compiler@21.1.0': {} - '@angular-eslint/eslint-plugin-template@21.1.0(@angular-eslint/template-parser@21.1.0(eslint@9.39.3)(typescript@5.9.3))(@typescript-eslint/types@8.56.1)(@typescript-eslint/utils@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3)(typescript@5.9.3)': + '@angular-eslint/eslint-plugin-template@21.1.0(@angular-eslint/template-parser@21.1.0(eslint@9.39.4)(typescript@5.9.3))(@typescript-eslint/types@8.57.0)(@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': dependencies: '@angular-eslint/bundled-angular-compiler': 21.1.0 - '@angular-eslint/template-parser': 21.1.0(eslint@9.39.3)(typescript@5.9.3) - '@angular-eslint/utils': 21.1.0(@typescript-eslint/utils@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3)(typescript@5.9.3) - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/utils': 8.56.1(eslint@9.39.3)(typescript@5.9.3) + '@angular-eslint/template-parser': 21.1.0(eslint@9.39.4)(typescript@5.9.3) + '@angular-eslint/utils': 21.1.0(@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/utils': 8.57.0(eslint@9.39.4)(typescript@5.9.3) aria-query: 5.3.2 axobject-query: 4.1.0 - eslint: 9.39.3 + eslint: 9.39.4 typescript: 5.9.3 - '@angular-eslint/eslint-plugin@21.1.0(@typescript-eslint/utils@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3)(typescript@5.9.3)': + '@angular-eslint/eslint-plugin@21.1.0(@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': dependencies: '@angular-eslint/bundled-angular-compiler': 21.1.0 - '@angular-eslint/utils': 21.1.0(@typescript-eslint/utils@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3)(typescript@5.9.3) - '@typescript-eslint/utils': 8.56.1(eslint@9.39.3)(typescript@5.9.3) - eslint: 9.39.3 + '@angular-eslint/utils': 21.1.0(@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/utils': 8.57.0(eslint@9.39.4)(typescript@5.9.3) + eslint: 9.39.4 ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 - '@angular-eslint/schematics@21.1.0(@angular-eslint/template-parser@21.1.0(eslint@9.39.3)(typescript@5.9.3))(@angular/cli@21.2.1(@types/node@25.3.5)(chokidar@5.0.0))(@typescript-eslint/types@8.56.1)(@typescript-eslint/utils@8.56.1(eslint@9.39.3)(typescript@5.9.3))(chokidar@5.0.0)(eslint@9.39.3)(typescript@5.9.3)': + '@angular-eslint/schematics@21.1.0(@angular-eslint/template-parser@21.1.0(eslint@9.39.4)(typescript@5.9.3))(@angular/cli@21.2.2(@types/node@25.5.0)(chokidar@5.0.0))(@typescript-eslint/types@8.57.0)(@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3))(chokidar@5.0.0)(eslint@9.39.4)(typescript@5.9.3)': dependencies: - '@angular-devkit/core': 21.2.1(chokidar@5.0.0) - '@angular-devkit/schematics': 21.2.1(chokidar@5.0.0) - '@angular-eslint/eslint-plugin': 21.1.0(@typescript-eslint/utils@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3)(typescript@5.9.3) - '@angular-eslint/eslint-plugin-template': 21.1.0(@angular-eslint/template-parser@21.1.0(eslint@9.39.3)(typescript@5.9.3))(@typescript-eslint/types@8.56.1)(@typescript-eslint/utils@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3)(typescript@5.9.3) - '@angular/cli': 21.2.1(@types/node@25.3.5)(chokidar@5.0.0) + '@angular-devkit/core': 21.2.2(chokidar@5.0.0) + '@angular-devkit/schematics': 21.2.2(chokidar@5.0.0) + '@angular-eslint/eslint-plugin': 21.1.0(@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) + '@angular-eslint/eslint-plugin-template': 21.1.0(@angular-eslint/template-parser@21.1.0(eslint@9.39.4)(typescript@5.9.3))(@typescript-eslint/types@8.57.0)(@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) + '@angular/cli': 21.2.2(@types/node@25.5.0)(chokidar@5.0.0) ignore: 7.0.5 semver: 7.7.3 strip-json-comments: 3.1.1 @@ -3616,36 +3620,36 @@ snapshots: - eslint - typescript - '@angular-eslint/template-parser@21.1.0(eslint@9.39.3)(typescript@5.9.3)': + '@angular-eslint/template-parser@21.1.0(eslint@9.39.4)(typescript@5.9.3)': dependencies: '@angular-eslint/bundled-angular-compiler': 21.1.0 - eslint: 9.39.3 - eslint-scope: 9.1.1 + eslint: 9.39.4 + eslint-scope: 9.1.2 typescript: 5.9.3 - '@angular-eslint/utils@21.1.0(@typescript-eslint/utils@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3)(typescript@5.9.3)': + '@angular-eslint/utils@21.1.0(@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': dependencies: '@angular-eslint/bundled-angular-compiler': 21.1.0 - '@typescript-eslint/utils': 8.56.1(eslint@9.39.3)(typescript@5.9.3) - eslint: 9.39.3 + '@typescript-eslint/utils': 8.57.0(eslint@9.39.4)(typescript@5.9.3) + eslint: 9.39.4 typescript: 5.9.3 - '@angular/animations@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))': + '@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))': dependencies: - '@angular/core': 21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0) tslib: 2.8.1 - '@angular/build@21.2.1(@angular/compiler-cli@21.2.1(@angular/compiler@21.2.1)(typescript@5.9.3))(@angular/compiler@21.2.1)(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/localize@21.2.1(@angular/compiler-cli@21.2.1(@angular/compiler@21.2.1)(typescript@5.9.3))(@angular/compiler@21.2.1))(@angular/platform-browser@21.2.1(@angular/animations@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/service-worker@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@types/node@25.3.5)(chokidar@5.0.0)(postcss@8.5.8)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.0.18(@types/node@25.3.5)(jsdom@27.4.0)(sass@1.97.3))': + '@angular/build@21.2.2(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/localize@21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4))(@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/service-worker@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@types/node@25.5.0)(chokidar@5.0.0)(postcss@8.5.8)(tslib@2.8.1)(typescript@5.9.3)(vitest@4.1.0(@types/node@25.5.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@25.5.0)(sass@1.97.3)))': dependencies: '@ampproject/remapping': 2.3.0 - '@angular-devkit/architect': 0.2102.1(chokidar@5.0.0) - '@angular/compiler': 21.2.1 - '@angular/compiler-cli': 21.2.1(@angular/compiler@21.2.1)(typescript@5.9.3) + '@angular-devkit/architect': 0.2102.2(chokidar@5.0.0) + '@angular/compiler': 21.2.4 + '@angular/compiler-cli': 21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3) '@babel/core': 7.29.0 '@babel/helper-annotate-as-pure': 7.27.3 '@babel/helper-split-export-declaration': 7.24.7 - '@inquirer/confirm': 5.1.21(@types/node@25.3.5) - '@vitejs/plugin-basic-ssl': 2.1.4(vite@7.3.1(@types/node@25.3.5)(sass@1.97.3)) + '@inquirer/confirm': 5.1.21(@types/node@25.5.0) + '@vitejs/plugin-basic-ssl': 2.1.4(vite@7.3.1(@types/node@25.5.0)(sass@1.97.3)) beasties: 0.4.1 browserslist: 4.28.1 esbuild: 0.27.3 @@ -3666,16 +3670,16 @@ snapshots: tslib: 2.8.1 typescript: 5.9.3 undici: 7.22.0 - vite: 7.3.1(@types/node@25.3.5)(sass@1.97.3) + vite: 7.3.1(@types/node@25.5.0)(sass@1.97.3) watchpack: 2.5.1 optionalDependencies: - '@angular/core': 21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0) - '@angular/localize': 21.2.1(@angular/compiler-cli@21.2.1(@angular/compiler@21.2.1)(typescript@5.9.3))(@angular/compiler@21.2.1) - '@angular/platform-browser': 21.2.1(@angular/animations@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)) - '@angular/service-worker': 21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0) + '@angular/localize': 21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4) + '@angular/platform-browser': 21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)) + '@angular/service-worker': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) lmdb: 3.5.1 postcss: 8.5.8 - vitest: 4.0.18(@types/node@25.3.5)(jsdom@27.4.0)(sass@1.97.3) + vitest: 4.1.0(@types/node@25.5.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@25.5.0)(sass@1.97.3)) transitivePeerDependencies: - '@types/node' - chokidar @@ -3689,15 +3693,15 @@ snapshots: - tsx - yaml - '@angular/cli@21.2.1(@types/node@25.3.5)(chokidar@5.0.0)': + '@angular/cli@21.2.2(@types/node@25.5.0)(chokidar@5.0.0)': dependencies: - '@angular-devkit/architect': 0.2102.1(chokidar@5.0.0) - '@angular-devkit/core': 21.2.1(chokidar@5.0.0) - '@angular-devkit/schematics': 21.2.1(chokidar@5.0.0) - '@inquirer/prompts': 7.10.1(@types/node@25.3.5) - '@listr2/prompt-adapter-inquirer': 3.0.5(@inquirer/prompts@7.10.1(@types/node@25.3.5))(@types/node@25.3.5)(listr2@9.0.5) + '@angular-devkit/architect': 0.2102.2(chokidar@5.0.0) + '@angular-devkit/core': 21.2.2(chokidar@5.0.0) + '@angular-devkit/schematics': 21.2.2(chokidar@5.0.0) + '@inquirer/prompts': 7.10.1(@types/node@25.5.0) + '@listr2/prompt-adapter-inquirer': 3.0.5(@inquirer/prompts@7.10.1(@types/node@25.5.0))(@types/node@25.5.0)(listr2@9.0.5) '@modelcontextprotocol/sdk': 1.26.0(zod@4.3.6) - '@schematics/angular': 21.2.1(chokidar@5.0.0) + '@schematics/angular': 21.2.2(chokidar@5.0.0) '@yarnpkg/lockfile': 1.1.0 algoliasearch: 5.48.1 ini: 6.0.0 @@ -3715,15 +3719,15 @@ snapshots: - chokidar - supports-color - '@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)': + '@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)': dependencies: - '@angular/core': 21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0) rxjs: 7.8.2 tslib: 2.8.1 - '@angular/compiler-cli@21.2.1(@angular/compiler@21.2.1)(typescript@5.9.3)': + '@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3)': dependencies: - '@angular/compiler': 21.2.1 + '@angular/compiler': 21.2.4 '@babel/core': 7.29.0 '@jridgewell/sourcemap-codec': 1.5.5 chokidar: 5.0.0 @@ -3737,31 +3741,31 @@ snapshots: transitivePeerDependencies: - supports-color - '@angular/compiler@21.2.1': + '@angular/compiler@21.2.4': dependencies: tslib: 2.8.1 - '@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)': + '@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)': dependencies: rxjs: 7.8.2 tslib: 2.8.1 optionalDependencies: - '@angular/compiler': 21.2.1 + '@angular/compiler': 21.2.4 zone.js: 0.15.0 - '@angular/forms@21.2.1(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.1(@angular/animations@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)': + '@angular/forms@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2)': dependencies: - '@angular/common': 21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) - '@angular/core': 21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0) - '@angular/platform-browser': 21.2.1(@angular/animations@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)) + '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0) + '@angular/platform-browser': 21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)) '@standard-schema/spec': 1.1.0 rxjs: 7.8.2 tslib: 2.8.1 - '@angular/localize@21.2.1(@angular/compiler-cli@21.2.1(@angular/compiler@21.2.1)(typescript@5.9.3))(@angular/compiler@21.2.1)': + '@angular/localize@21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4)': dependencies: - '@angular/compiler': 21.2.1 - '@angular/compiler-cli': 21.2.1(@angular/compiler@21.2.1)(typescript@5.9.3) + '@angular/compiler': 21.2.4 + '@angular/compiler-cli': 21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3) '@babel/core': 7.29.0 '@types/babel__core': 7.20.5 tinyglobby: 0.2.15 @@ -3769,25 +3773,25 @@ snapshots: transitivePeerDependencies: - supports-color - '@angular/platform-browser-dynamic@21.2.1(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/compiler@21.2.1)(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.1(@angular/animations@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))': + '@angular/platform-browser-dynamic@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/compiler@21.2.4)(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))': dependencies: - '@angular/common': 21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) - '@angular/compiler': 21.2.1 - '@angular/core': 21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0) - '@angular/platform-browser': 21.2.1(@angular/animations@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)) + '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) + '@angular/compiler': 21.2.4 + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0) + '@angular/platform-browser': 21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)) tslib: 2.8.1 - '@angular/platform-browser@21.2.1(@angular/animations@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))': + '@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))': dependencies: - '@angular/common': 21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) - '@angular/core': 21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0) + '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0) tslib: 2.8.1 optionalDependencies: - '@angular/animations': 21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)) + '@angular/animations': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)) - '@angular/service-worker@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)': + '@angular/service-worker@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2)': dependencies: - '@angular/core': 21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0) rxjs: 7.8.2 tslib: 2.8.1 @@ -3797,7 +3801,7 @@ snapshots: '@csstools/css-color-parser': 4.0.2(@csstools/css-parser-algorithms@4.0.0(@csstools/css-tokenizer@4.0.0))(@csstools/css-tokenizer@4.0.0) '@csstools/css-parser-algorithms': 4.0.0(@csstools/css-tokenizer@4.0.0) '@csstools/css-tokenizer': 4.0.0 - lru-cache: 11.2.6 + lru-cache: 11.2.7 '@asamuzakjp/dom-selector@6.8.1': dependencies: @@ -3805,7 +3809,7 @@ snapshots: bidi-js: 1.0.3 css-tree: 3.2.1 is-potential-custom-element-name: 1.0.1 - lru-cache: 11.2.6 + lru-cache: 11.2.7 '@asamuzakjp/nwsapi@2.3.9': {} @@ -3939,18 +3943,18 @@ snapshots: '@csstools/css-tokenizer@4.0.0': {} - '@emnapi/core@1.8.1': + '@emnapi/core@1.9.0': dependencies: - '@emnapi/wasi-threads': 1.1.0 + '@emnapi/wasi-threads': 1.2.0 tslib: 2.8.1 optional: true - '@emnapi/runtime@1.8.1': + '@emnapi/runtime@1.9.0': dependencies: tslib: 2.8.1 optional: true - '@emnapi/wasi-threads@1.1.0': + '@emnapi/wasi-threads@1.2.0': dependencies: tslib: 2.8.1 optional: true @@ -4033,14 +4037,14 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true - '@eslint-community/eslint-utils@4.9.1(eslint@9.39.3)': + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': dependencies: - eslint: 9.39.3 + eslint: 9.39.4 eslint-visitor-keys: 3.4.3 '@eslint-community/regexpp@4.12.2': {} - '@eslint/config-array@0.21.1': + '@eslint/config-array@0.21.2': dependencies: '@eslint/object-schema': 2.1.7 debug: 4.4.3 @@ -4056,7 +4060,7 @@ snapshots: dependencies: '@types/json-schema': 7.0.15 - '@eslint/eslintrc@3.3.4': + '@eslint/eslintrc@3.3.5': dependencies: ajv: 6.14.0 debug: 4.4.3 @@ -4070,7 +4074,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@eslint/js@9.39.3': {} + '@eslint/js@9.39.4': {} '@eslint/object-schema@2.1.7': {} @@ -4081,9 +4085,9 @@ snapshots: '@exodus/bytes@1.15.0': {} - '@fortawesome/angular-fontawesome@4.0.0(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))': + '@fortawesome/angular-fontawesome@4.0.0(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))': dependencies: - '@angular/core': 21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0) '@fortawesome/fontawesome-svg-core': 7.2.0 tslib: 2.8.1 @@ -4112,9 +4116,9 @@ snapshots: '@harperfast/extended-iterable@1.0.3': optional: true - '@hono/node-server@1.19.11(hono@4.12.5)': + '@hono/node-server@1.19.11(hono@4.12.8)': dependencies: - hono: 4.12.5 + hono: 4.12.8 '@humanfs/core@0.19.1': {} @@ -4129,128 +4133,128 @@ snapshots: '@inquirer/ansi@1.0.2': {} - '@inquirer/checkbox@4.3.2(@types/node@25.3.5)': + '@inquirer/checkbox@4.3.2(@types/node@25.5.0)': dependencies: '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@25.3.5) + '@inquirer/core': 10.3.2(@types/node@25.5.0) '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@25.3.5) + '@inquirer/type': 3.0.10(@types/node@25.5.0) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 25.3.5 + '@types/node': 25.5.0 - '@inquirer/confirm@5.1.21(@types/node@25.3.5)': + '@inquirer/confirm@5.1.21(@types/node@25.5.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@25.3.5) - '@inquirer/type': 3.0.10(@types/node@25.3.5) + '@inquirer/core': 10.3.2(@types/node@25.5.0) + '@inquirer/type': 3.0.10(@types/node@25.5.0) optionalDependencies: - '@types/node': 25.3.5 + '@types/node': 25.5.0 - '@inquirer/core@10.3.2(@types/node@25.3.5)': + '@inquirer/core@10.3.2(@types/node@25.5.0)': dependencies: '@inquirer/ansi': 1.0.2 '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@25.3.5) + '@inquirer/type': 3.0.10(@types/node@25.5.0) cli-width: 4.1.0 mute-stream: 2.0.0 signal-exit: 4.1.0 wrap-ansi: 6.2.0 yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 25.3.5 + '@types/node': 25.5.0 - '@inquirer/editor@4.2.23(@types/node@25.3.5)': + '@inquirer/editor@4.2.23(@types/node@25.5.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@25.3.5) - '@inquirer/external-editor': 1.0.3(@types/node@25.3.5) - '@inquirer/type': 3.0.10(@types/node@25.3.5) + '@inquirer/core': 10.3.2(@types/node@25.5.0) + '@inquirer/external-editor': 1.0.3(@types/node@25.5.0) + '@inquirer/type': 3.0.10(@types/node@25.5.0) optionalDependencies: - '@types/node': 25.3.5 + '@types/node': 25.5.0 - '@inquirer/expand@4.0.23(@types/node@25.3.5)': + '@inquirer/expand@4.0.23(@types/node@25.5.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@25.3.5) - '@inquirer/type': 3.0.10(@types/node@25.3.5) + '@inquirer/core': 10.3.2(@types/node@25.5.0) + '@inquirer/type': 3.0.10(@types/node@25.5.0) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 25.3.5 + '@types/node': 25.5.0 - '@inquirer/external-editor@1.0.3(@types/node@25.3.5)': + '@inquirer/external-editor@1.0.3(@types/node@25.5.0)': dependencies: chardet: 2.1.1 iconv-lite: 0.7.2 optionalDependencies: - '@types/node': 25.3.5 + '@types/node': 25.5.0 '@inquirer/figures@1.0.15': {} - '@inquirer/input@4.3.1(@types/node@25.3.5)': + '@inquirer/input@4.3.1(@types/node@25.5.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@25.3.5) - '@inquirer/type': 3.0.10(@types/node@25.3.5) + '@inquirer/core': 10.3.2(@types/node@25.5.0) + '@inquirer/type': 3.0.10(@types/node@25.5.0) optionalDependencies: - '@types/node': 25.3.5 + '@types/node': 25.5.0 - '@inquirer/number@3.0.23(@types/node@25.3.5)': + '@inquirer/number@3.0.23(@types/node@25.5.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@25.3.5) - '@inquirer/type': 3.0.10(@types/node@25.3.5) + '@inquirer/core': 10.3.2(@types/node@25.5.0) + '@inquirer/type': 3.0.10(@types/node@25.5.0) optionalDependencies: - '@types/node': 25.3.5 + '@types/node': 25.5.0 - '@inquirer/password@4.0.23(@types/node@25.3.5)': + '@inquirer/password@4.0.23(@types/node@25.5.0)': dependencies: '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@25.3.5) - '@inquirer/type': 3.0.10(@types/node@25.3.5) + '@inquirer/core': 10.3.2(@types/node@25.5.0) + '@inquirer/type': 3.0.10(@types/node@25.5.0) optionalDependencies: - '@types/node': 25.3.5 + '@types/node': 25.5.0 - '@inquirer/prompts@7.10.1(@types/node@25.3.5)': + '@inquirer/prompts@7.10.1(@types/node@25.5.0)': dependencies: - '@inquirer/checkbox': 4.3.2(@types/node@25.3.5) - '@inquirer/confirm': 5.1.21(@types/node@25.3.5) - '@inquirer/editor': 4.2.23(@types/node@25.3.5) - '@inquirer/expand': 4.0.23(@types/node@25.3.5) - '@inquirer/input': 4.3.1(@types/node@25.3.5) - '@inquirer/number': 3.0.23(@types/node@25.3.5) - '@inquirer/password': 4.0.23(@types/node@25.3.5) - '@inquirer/rawlist': 4.1.11(@types/node@25.3.5) - '@inquirer/search': 3.2.2(@types/node@25.3.5) - '@inquirer/select': 4.4.2(@types/node@25.3.5) + '@inquirer/checkbox': 4.3.2(@types/node@25.5.0) + '@inquirer/confirm': 5.1.21(@types/node@25.5.0) + '@inquirer/editor': 4.2.23(@types/node@25.5.0) + '@inquirer/expand': 4.0.23(@types/node@25.5.0) + '@inquirer/input': 4.3.1(@types/node@25.5.0) + '@inquirer/number': 3.0.23(@types/node@25.5.0) + '@inquirer/password': 4.0.23(@types/node@25.5.0) + '@inquirer/rawlist': 4.1.11(@types/node@25.5.0) + '@inquirer/search': 3.2.2(@types/node@25.5.0) + '@inquirer/select': 4.4.2(@types/node@25.5.0) optionalDependencies: - '@types/node': 25.3.5 + '@types/node': 25.5.0 - '@inquirer/rawlist@4.1.11(@types/node@25.3.5)': + '@inquirer/rawlist@4.1.11(@types/node@25.5.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@25.3.5) - '@inquirer/type': 3.0.10(@types/node@25.3.5) + '@inquirer/core': 10.3.2(@types/node@25.5.0) + '@inquirer/type': 3.0.10(@types/node@25.5.0) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 25.3.5 + '@types/node': 25.5.0 - '@inquirer/search@3.2.2(@types/node@25.3.5)': + '@inquirer/search@3.2.2(@types/node@25.5.0)': dependencies: - '@inquirer/core': 10.3.2(@types/node@25.3.5) + '@inquirer/core': 10.3.2(@types/node@25.5.0) '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@25.3.5) + '@inquirer/type': 3.0.10(@types/node@25.5.0) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 25.3.5 + '@types/node': 25.5.0 - '@inquirer/select@4.4.2(@types/node@25.3.5)': + '@inquirer/select@4.4.2(@types/node@25.5.0)': dependencies: '@inquirer/ansi': 1.0.2 - '@inquirer/core': 10.3.2(@types/node@25.3.5) + '@inquirer/core': 10.3.2(@types/node@25.5.0) '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@25.3.5) + '@inquirer/type': 3.0.10(@types/node@25.5.0) yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 25.3.5 + '@types/node': 25.5.0 - '@inquirer/type@3.0.10(@types/node@25.3.5)': + '@inquirer/type@3.0.10(@types/node@25.5.0)': optionalDependencies: - '@types/node': 25.3.5 + '@types/node': 25.5.0 '@isaacs/fs-minipass@4.0.1': dependencies: @@ -4277,10 +4281,10 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@listr2/prompt-adapter-inquirer@3.0.5(@inquirer/prompts@7.10.1(@types/node@25.3.5))(@types/node@25.3.5)(listr2@9.0.5)': + '@listr2/prompt-adapter-inquirer@3.0.5(@inquirer/prompts@7.10.1(@types/node@25.5.0))(@types/node@25.5.0)(listr2@9.0.5)': dependencies: - '@inquirer/prompts': 7.10.1(@types/node@25.3.5) - '@inquirer/type': 3.0.10(@types/node@25.3.5) + '@inquirer/prompts': 7.10.1(@types/node@25.5.0) + '@inquirer/type': 3.0.10(@types/node@25.5.0) listr2: 9.0.5 transitivePeerDependencies: - '@types/node' @@ -4308,7 +4312,7 @@ snapshots: '@modelcontextprotocol/sdk@1.26.0(zod@4.3.6)': dependencies: - '@hono/node-server': 1.19.11(hono@4.12.5) + '@hono/node-server': 1.19.11(hono@4.12.8) ajv: 8.18.0 ajv-formats: 3.0.1(ajv@8.18.0) content-type: 1.0.5 @@ -4317,9 +4321,9 @@ snapshots: eventsource: 3.0.7 eventsource-parser: 3.0.6 express: 5.2.1 - express-rate-limit: 8.3.0(express@5.2.1) - hono: 4.12.5 - jose: 6.2.0 + express-rate-limit: 8.3.1(express@5.2.1) + hono: 4.12.8 + jose: 6.2.1 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 raw-body: 3.0.2 @@ -4420,26 +4424,26 @@ snapshots: '@napi-rs/wasm-runtime@1.1.1': dependencies: - '@emnapi/core': 1.8.1 - '@emnapi/runtime': 1.8.1 + '@emnapi/core': 1.9.0 + '@emnapi/runtime': 1.9.0 '@tybys/wasm-util': 0.10.1 optional: true - '@ng-bootstrap/ng-bootstrap@20.0.0(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/forms@21.2.1(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.1(@angular/animations@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2))(@angular/localize@21.2.1(@angular/compiler-cli@21.2.1(@angular/compiler@21.2.1)(typescript@5.9.3))(@angular/compiler@21.2.1))(@popperjs/core@2.11.8)(rxjs@7.8.2)': + '@ng-bootstrap/ng-bootstrap@20.0.0(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/forms@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2))(@angular/localize@21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4))(@popperjs/core@2.11.8)(rxjs@7.8.2)': dependencies: - '@angular/common': 21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) - '@angular/core': 21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0) - '@angular/forms': 21.2.1(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.1(@angular/animations@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) - '@angular/localize': 21.2.1(@angular/compiler-cli@21.2.1(@angular/compiler@21.2.1)(typescript@5.9.3))(@angular/compiler@21.2.1) + '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0) + '@angular/forms': 21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) + '@angular/localize': 21.2.4(@angular/compiler-cli@21.2.4(@angular/compiler@21.2.4)(typescript@5.9.3))(@angular/compiler@21.2.4) '@popperjs/core': 2.11.8 rxjs: 7.8.2 tslib: 2.8.1 - '@ng-select/ng-select@21.5.2(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/forms@21.2.1(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.1(@angular/animations@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2))': + '@ng-select/ng-select@21.5.2(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/forms@21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2))': dependencies: - '@angular/common': 21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) - '@angular/core': 21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0) - '@angular/forms': 21.2.1(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.1(@angular/animations@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) + '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0) + '@angular/forms': 21.2.4(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(@angular/platform-browser@21.2.4(@angular/animations@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)))(rxjs@7.8.2) tslib: 2.8.1 '@nodelib/fs.scandir@2.1.5': @@ -4459,7 +4463,7 @@ snapshots: agent-base: 7.1.4 http-proxy-agent: 7.0.2 https-proxy-agent: 7.0.6 - lru-cache: 11.2.6 + lru-cache: 11.2.7 socks-proxy-agent: 8.0.5 transitivePeerDependencies: - supports-color @@ -4473,7 +4477,7 @@ snapshots: '@gar/promise-retry': 1.0.2 '@npmcli/promise-spawn': 9.0.1 ini: 6.0.0 - lru-cache: 11.2.6 + lru-cache: 11.2.7 npm-pick-manifest: 11.0.3 proc-log: 6.1.0 semver: 7.7.4 @@ -4695,10 +4699,10 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.59.0': optional: true - '@schematics/angular@21.2.1(chokidar@5.0.0)': + '@schematics/angular@21.2.2(chokidar@5.0.0)': dependencies: - '@angular-devkit/core': 21.2.1(chokidar@5.0.0) - '@angular-devkit/schematics': 21.2.1(chokidar@5.0.0) + '@angular-devkit/core': 21.2.2(chokidar@5.0.0) + '@angular-devkit/schematics': 21.2.2(chokidar@5.0.0) jsonc-parser: 3.3.1 transitivePeerDependencies: - chokidar @@ -4779,7 +4783,7 @@ snapshots: '@types/cors@2.8.19': dependencies: - '@types/node': 25.3.5 + '@types/node': 25.5.0 '@types/deep-eql@4.0.2': {} @@ -4789,19 +4793,23 @@ snapshots: '@types/json-schema@7.0.15': {} - '@types/node@25.3.5': + '@types/node@25.5.0': dependencies: undici-types: 7.18.2 - '@typescript-eslint/eslint-plugin@8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3)(typescript@5.9.3)': + '@types/ws@8.18.1': + dependencies: + '@types/node': 25.5.0 + + '@typescript-eslint/eslint-plugin@8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3)': dependencies: '@eslint-community/regexpp': 4.12.2 - '@typescript-eslint/parser': 8.47.0(eslint@9.39.3)(typescript@5.9.3) + '@typescript-eslint/parser': 8.47.0(eslint@9.39.4)(typescript@5.9.3) '@typescript-eslint/scope-manager': 8.47.0 - '@typescript-eslint/type-utils': 8.47.0(eslint@9.39.3)(typescript@5.9.3) - '@typescript-eslint/utils': 8.47.0(eslint@9.39.3)(typescript@5.9.3) + '@typescript-eslint/type-utils': 8.47.0(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/utils': 8.47.0(eslint@9.39.4)(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.47.0 - eslint: 9.39.3 + eslint: 9.39.4 graphemer: 1.4.0 ignore: 7.0.5 natural-compare: 1.4.0 @@ -4810,14 +4818,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@8.47.0(eslint@9.39.3)(typescript@5.9.3)': + '@typescript-eslint/parser@8.47.0(eslint@9.39.4)(typescript@5.9.3)': dependencies: '@typescript-eslint/scope-manager': 8.47.0 '@typescript-eslint/types': 8.47.0 '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3) '@typescript-eslint/visitor-keys': 8.47.0 debug: 4.4.3 - eslint: 9.39.3 + eslint: 9.39.4 typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -4831,10 +4839,10 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/project-service@8.56.1(typescript@5.9.3)': + '@typescript-eslint/project-service@8.57.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) - '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/tsconfig-utils': 8.57.0(typescript@5.9.3) + '@typescript-eslint/types': 8.57.0 debug: 4.4.3 typescript: 5.9.3 transitivePeerDependencies: @@ -4845,26 +4853,26 @@ snapshots: '@typescript-eslint/types': 8.47.0 '@typescript-eslint/visitor-keys': 8.47.0 - '@typescript-eslint/scope-manager@8.56.1': + '@typescript-eslint/scope-manager@8.57.0': dependencies: - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/visitor-keys': 8.56.1 + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/visitor-keys': 8.57.0 '@typescript-eslint/tsconfig-utils@8.47.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/tsconfig-utils@8.56.1(typescript@5.9.3)': + '@typescript-eslint/tsconfig-utils@8.57.0(typescript@5.9.3)': dependencies: typescript: 5.9.3 - '@typescript-eslint/type-utils@8.47.0(eslint@9.39.3)(typescript@5.9.3)': + '@typescript-eslint/type-utils@8.47.0(eslint@9.39.4)(typescript@5.9.3)': dependencies: '@typescript-eslint/types': 8.47.0 '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.47.0(eslint@9.39.3)(typescript@5.9.3) + '@typescript-eslint/utils': 8.47.0(eslint@9.39.4)(typescript@5.9.3) debug: 4.4.3 - eslint: 9.39.3 + eslint: 9.39.4 ts-api-utils: 2.4.0(typescript@5.9.3) typescript: 5.9.3 transitivePeerDependencies: @@ -4872,7 +4880,7 @@ snapshots: '@typescript-eslint/types@8.47.0': {} - '@typescript-eslint/types@8.56.1': {} + '@typescript-eslint/types@8.57.0': {} '@typescript-eslint/typescript-estree@8.47.0(typescript@5.9.3)': dependencies: @@ -4890,12 +4898,12 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/typescript-estree@8.56.1(typescript@5.9.3)': + '@typescript-eslint/typescript-estree@8.57.0(typescript@5.9.3)': dependencies: - '@typescript-eslint/project-service': 8.56.1(typescript@5.9.3) - '@typescript-eslint/tsconfig-utils': 8.56.1(typescript@5.9.3) - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/visitor-keys': 8.56.1 + '@typescript-eslint/project-service': 8.57.0(typescript@5.9.3) + '@typescript-eslint/tsconfig-utils': 8.57.0(typescript@5.9.3) + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/visitor-keys': 8.57.0 debug: 4.4.3 minimatch: 10.2.4 semver: 7.7.4 @@ -4905,24 +4913,24 @@ snapshots: transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.47.0(eslint@9.39.3)(typescript@5.9.3)': + '@typescript-eslint/utils@8.47.0(eslint@9.39.4)(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) '@typescript-eslint/scope-manager': 8.47.0 '@typescript-eslint/types': 8.47.0 '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3) - eslint: 9.39.3 + eslint: 9.39.4 typescript: 5.9.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@8.56.1(eslint@9.39.3)(typescript@5.9.3)': + '@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3)': dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3) - '@typescript-eslint/scope-manager': 8.56.1 - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/typescript-estree': 8.56.1(typescript@5.9.3) - eslint: 9.39.3 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@typescript-eslint/scope-manager': 8.57.0 + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/typescript-estree': 8.57.0(typescript@5.9.3) + eslint: 9.39.4 typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -4932,53 +4940,55 @@ snapshots: '@typescript-eslint/types': 8.47.0 eslint-visitor-keys: 4.2.1 - '@typescript-eslint/visitor-keys@8.56.1': + '@typescript-eslint/visitor-keys@8.57.0': dependencies: - '@typescript-eslint/types': 8.56.1 + '@typescript-eslint/types': 8.57.0 eslint-visitor-keys: 5.0.1 - '@vitejs/plugin-basic-ssl@2.1.4(vite@7.3.1(@types/node@25.3.5)(sass@1.97.3))': + '@vitejs/plugin-basic-ssl@2.1.4(vite@7.3.1(@types/node@25.5.0)(sass@1.97.3))': dependencies: - vite: 7.3.1(@types/node@25.3.5)(sass@1.97.3) + vite: 7.3.1(@types/node@25.5.0)(sass@1.97.3) - '@vitest/expect@4.0.18': + '@vitest/expect@4.1.0': dependencies: '@standard-schema/spec': 1.1.0 '@types/chai': 5.2.3 - '@vitest/spy': 4.0.18 - '@vitest/utils': 4.0.18 + '@vitest/spy': 4.1.0 + '@vitest/utils': 4.1.0 chai: 6.2.2 - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 - '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.3.5)(sass@1.97.3))': + '@vitest/mocker@4.1.0(vite@7.3.1(@types/node@25.5.0)(sass@1.97.3))': dependencies: - '@vitest/spy': 4.0.18 + '@vitest/spy': 4.1.0 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.3.5)(sass@1.97.3) + vite: 7.3.1(@types/node@25.5.0)(sass@1.97.3) - '@vitest/pretty-format@4.0.18': + '@vitest/pretty-format@4.1.0': dependencies: - tinyrainbow: 3.0.3 + tinyrainbow: 3.1.0 - '@vitest/runner@4.0.18': + '@vitest/runner@4.1.0': dependencies: - '@vitest/utils': 4.0.18 + '@vitest/utils': 4.1.0 pathe: 2.0.3 - '@vitest/snapshot@4.0.18': + '@vitest/snapshot@4.1.0': dependencies: - '@vitest/pretty-format': 4.0.18 + '@vitest/pretty-format': 4.1.0 + '@vitest/utils': 4.1.0 magic-string: 0.30.21 pathe: 2.0.3 - '@vitest/spy@4.0.18': {} + '@vitest/spy@4.1.0': {} - '@vitest/utils@4.0.18': + '@vitest/utils@4.1.0': dependencies: - '@vitest/pretty-format': 4.0.18 - tinyrainbow: 3.0.3 + '@vitest/pretty-format': 4.1.0 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 '@yarnpkg/lockfile@1.1.0': {} @@ -5037,21 +5047,21 @@ snapshots: '@algolia/requester-fetch': 5.48.1 '@algolia/requester-node-http': 5.48.1 - angular-eslint@21.1.0(@angular/cli@21.2.1(@types/node@25.3.5)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@9.39.3)(typescript-eslint@8.47.0(eslint@9.39.3)(typescript@5.9.3))(typescript@5.9.3): + angular-eslint@21.1.0(@angular/cli@21.2.2(@types/node@25.5.0)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@9.39.4)(typescript-eslint@8.47.0(eslint@9.39.4)(typescript@5.9.3))(typescript@5.9.3): dependencies: - '@angular-devkit/core': 21.2.1(chokidar@5.0.0) - '@angular-devkit/schematics': 21.2.1(chokidar@5.0.0) - '@angular-eslint/builder': 21.1.0(@angular/cli@21.2.1(@types/node@25.3.5)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@9.39.3)(typescript@5.9.3) - '@angular-eslint/eslint-plugin': 21.1.0(@typescript-eslint/utils@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3)(typescript@5.9.3) - '@angular-eslint/eslint-plugin-template': 21.1.0(@angular-eslint/template-parser@21.1.0(eslint@9.39.3)(typescript@5.9.3))(@typescript-eslint/types@8.56.1)(@typescript-eslint/utils@8.56.1(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3)(typescript@5.9.3) - '@angular-eslint/schematics': 21.1.0(@angular-eslint/template-parser@21.1.0(eslint@9.39.3)(typescript@5.9.3))(@angular/cli@21.2.1(@types/node@25.3.5)(chokidar@5.0.0))(@typescript-eslint/types@8.56.1)(@typescript-eslint/utils@8.56.1(eslint@9.39.3)(typescript@5.9.3))(chokidar@5.0.0)(eslint@9.39.3)(typescript@5.9.3) - '@angular-eslint/template-parser': 21.1.0(eslint@9.39.3)(typescript@5.9.3) - '@angular/cli': 21.2.1(@types/node@25.3.5)(chokidar@5.0.0) - '@typescript-eslint/types': 8.56.1 - '@typescript-eslint/utils': 8.56.1(eslint@9.39.3)(typescript@5.9.3) - eslint: 9.39.3 + '@angular-devkit/core': 21.2.2(chokidar@5.0.0) + '@angular-devkit/schematics': 21.2.2(chokidar@5.0.0) + '@angular-eslint/builder': 21.1.0(@angular/cli@21.2.2(@types/node@25.5.0)(chokidar@5.0.0))(chokidar@5.0.0)(eslint@9.39.4)(typescript@5.9.3) + '@angular-eslint/eslint-plugin': 21.1.0(@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) + '@angular-eslint/eslint-plugin-template': 21.1.0(@angular-eslint/template-parser@21.1.0(eslint@9.39.4)(typescript@5.9.3))(@typescript-eslint/types@8.57.0)(@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) + '@angular-eslint/schematics': 21.1.0(@angular-eslint/template-parser@21.1.0(eslint@9.39.4)(typescript@5.9.3))(@angular/cli@21.2.2(@types/node@25.5.0)(chokidar@5.0.0))(@typescript-eslint/types@8.57.0)(@typescript-eslint/utils@8.57.0(eslint@9.39.4)(typescript@5.9.3))(chokidar@5.0.0)(eslint@9.39.4)(typescript@5.9.3) + '@angular-eslint/template-parser': 21.1.0(eslint@9.39.4)(typescript@5.9.3) + '@angular/cli': 21.2.2(@types/node@25.5.0)(chokidar@5.0.0) + '@typescript-eslint/types': 8.57.0 + '@typescript-eslint/utils': 8.57.0(eslint@9.39.4)(typescript@5.9.3) + eslint: 9.39.4 typescript: 5.9.3 - typescript-eslint: 8.47.0(eslint@9.39.3)(typescript@5.9.3) + typescript-eslint: 8.47.0(eslint@9.39.4)(typescript@5.9.3) transitivePeerDependencies: - chokidar - supports-color @@ -5084,7 +5094,7 @@ snapshots: base64id@2.0.0: {} - baseline-browser-mapping@2.10.0: {} + baseline-browser-mapping@2.10.7: {} beasties@0.4.1: dependencies: @@ -5141,9 +5151,9 @@ snapshots: browserslist@4.28.1: dependencies: - baseline-browser-mapping: 2.10.0 - caniuse-lite: 1.0.30001777 - electron-to-chromium: 1.5.307 + baseline-browser-mapping: 2.10.7 + caniuse-lite: 1.0.30001778 + electron-to-chromium: 1.5.313 node-releases: 2.0.36 update-browserslist-db: 1.2.3(browserslist@4.28.1) @@ -5156,7 +5166,7 @@ snapshots: '@npmcli/fs': 5.0.0 fs-minipass: 3.0.3 glob: 13.0.6 - lru-cache: 11.2.6 + lru-cache: 11.2.7 minipass: 7.1.3 minipass-collect: 2.0.1 minipass-flush: 1.0.5 @@ -5177,7 +5187,7 @@ snapshots: callsites@3.1.0: {} - caniuse-lite@1.0.30001777: {} + caniuse-lite@1.0.30001778: {} chai@6.2.2: {} @@ -5274,7 +5284,7 @@ snapshots: '@asamuzakjp/css-color': 4.1.2 '@csstools/css-syntax-patches-for-csstree': 1.1.0 css-tree: 3.2.1 - lru-cache: 11.2.6 + lru-cache: 11.2.7 data-urls@6.0.1: dependencies: @@ -5320,7 +5330,7 @@ snapshots: ee-first@1.1.1: {} - electron-to-chromium@1.5.307: {} + electron-to-chromium@1.5.313: {} emoji-regex@10.6.0: {} @@ -5342,10 +5352,11 @@ snapshots: engine.io-parser@5.2.3: {} - engine.io@6.6.5: + engine.io@6.6.6: dependencies: '@types/cors': 2.8.19 - '@types/node': 25.3.5 + '@types/node': 25.5.0 + '@types/ws': 8.18.1 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.7.2 @@ -5374,7 +5385,7 @@ snapshots: es-errors@1.3.0: {} - es-module-lexer@1.7.0: {} + es-module-lexer@2.0.0: {} es-object-atoms@1.1.1: dependencies: @@ -5420,7 +5431,7 @@ snapshots: esrecurse: 4.3.0 estraverse: 5.3.0 - eslint-scope@9.1.1: + eslint-scope@9.1.2: dependencies: '@types/esrecurse': 4.3.1 '@types/estree': 1.0.8 @@ -5433,15 +5444,15 @@ snapshots: eslint-visitor-keys@5.0.1: {} - eslint@9.39.3: + eslint@9.39.4: dependencies: - '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.3) + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) '@eslint-community/regexpp': 4.12.2 - '@eslint/config-array': 0.21.1 + '@eslint/config-array': 0.21.2 '@eslint/config-helpers': 0.4.2 '@eslint/core': 0.17.0 - '@eslint/eslintrc': 3.3.4 - '@eslint/js': 9.39.3 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 '@eslint/plugin-kit': 0.4.1 '@humanfs/node': 0.16.7 '@humanwhocodes/module-importer': 1.0.1 @@ -5508,7 +5519,7 @@ snapshots: exponential-backoff@3.1.3: {} - express-rate-limit@8.3.0(express@5.2.1): + express-rate-limit@8.3.1(express@5.2.1): dependencies: express: 5.2.1 ip-address: 10.1.0 @@ -5596,10 +5607,10 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.3.4 + flatted: 3.4.1 keyv: 4.5.4 - flatted@3.3.4: {} + flatted@3.4.1: {} forwarded@0.2.0: {} @@ -5670,11 +5681,11 @@ snapshots: dependencies: function-bind: 1.1.2 - hono@4.12.5: {} + hono@4.12.8: {} hosted-git-info@9.0.2: dependencies: - lru-cache: 11.2.6 + lru-cache: 11.2.7 html-encoding-sniffer@6.0.0: dependencies: @@ -5780,7 +5791,7 @@ snapshots: transitivePeerDependencies: - supports-color - jose@6.2.0: {} + jose@6.2.1: {} js-tokens@4.0.0: {} @@ -5803,7 +5814,7 @@ snapshots: parse5: 8.0.0 saxes: 6.0.0 symbol-tree: 3.2.4 - tough-cookie: 6.0.0 + tough-cookie: 6.0.1 w3c-xmlserializer: 5.0.0 webidl-conversions: 8.0.1 whatwg-mimetype: 4.0.0 @@ -5857,7 +5868,7 @@ snapshots: lmdb@3.5.1: dependencies: '@harperfast/extended-iterable': 1.0.3 - msgpackr: 1.11.8 + msgpackr: 1.11.9 node-addon-api: 6.1.0 node-gyp-build-optional-packages: 5.2.2 ordered-binary: 1.6.1 @@ -5891,7 +5902,7 @@ snapshots: strip-ansi: 7.2.0 wrap-ansi: 9.0.2 - lru-cache@11.2.6: {} + lru-cache@11.2.7: {} lru-cache@5.1.1: dependencies: @@ -6008,7 +6019,7 @@ snapshots: '@msgpackr-extract/msgpackr-extract-win32-x64': 3.0.3 optional: true - msgpackr@1.11.8: + msgpackr@1.11.9: optionalDependencies: msgpackr-extract: 3.0.3 optional: true @@ -6023,16 +6034,16 @@ snapshots: negotiator@1.0.0: {} - ngx-cookie-service@21.1.0(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0)): + ngx-cookie-service@21.1.0(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0)): dependencies: - '@angular/common': 21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) - '@angular/core': 21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0) + '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0) tslib: 2.8.1 - ngx-socket-io@4.10.0(@angular/common@21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2): + ngx-socket-io@4.10.0(@angular/common@21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2))(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2): dependencies: - '@angular/common': 21.2.1(@angular/core@21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) - '@angular/core': 21.2.1(@angular/compiler@21.2.1)(rxjs@7.8.2)(zone.js@0.15.0) + '@angular/common': 21.2.4(@angular/core@21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0))(rxjs@7.8.2) + '@angular/core': 21.2.4(@angular/compiler@21.2.4)(rxjs@7.8.2)(zone.js@0.15.0) core-js: 3.48.0 reflect-metadata: 0.2.2 rxjs: 7.8.2 @@ -6064,7 +6075,7 @@ snapshots: nopt: 9.0.0 proc-log: 6.1.0 semver: 7.7.4 - tar: 7.5.10 + tar: 7.5.11 tinyglobby: 0.2.15 which: 6.0.1 transitivePeerDependencies: @@ -6191,7 +6202,7 @@ snapshots: promise-retry: 2.0.1 sigstore: 4.1.0 ssri: 13.0.1 - tar: 7.5.10 + tar: 7.5.11 transitivePeerDependencies: - supports-color @@ -6221,7 +6232,7 @@ snapshots: path-scurry@2.0.2: dependencies: - lru-cache: 11.2.6 + lru-cache: 11.2.7 minipass: 7.1.3 path-to-regexp@8.3.0: {} @@ -6515,7 +6526,7 @@ snapshots: base64id: 2.0.0 cors: 2.8.6 debug: 4.4.3 - engine.io: 6.6.5 + engine.io: 6.6.6 socket.io-adapter: 2.5.6 socket.io-parser: 4.2.5 transitivePeerDependencies: @@ -6564,7 +6575,7 @@ snapshots: statuses@2.0.2: {} - std-env@3.10.0: {} + std-env@4.0.0: {} stdin-discarder@0.3.1: {} @@ -6601,7 +6612,7 @@ snapshots: symbol-tree@3.2.4: {} - tar@7.5.10: + tar@7.5.11: dependencies: '@isaacs/fs-minipass': 4.0.1 chownr: 3.0.0 @@ -6611,20 +6622,20 @@ snapshots: tinybench@2.9.0: {} - tinyexec@1.0.2: {} + tinyexec@1.0.4: {} tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 - tinyrainbow@3.0.3: {} + tinyrainbow@3.1.0: {} - tldts-core@7.0.24: {} + tldts-core@7.0.25: {} - tldts@7.0.24: + tldts@7.0.25: dependencies: - tldts-core: 7.0.24 + tldts-core: 7.0.25 to-regex-range@5.0.1: dependencies: @@ -6632,9 +6643,9 @@ snapshots: toidentifier@1.0.1: {} - tough-cookie@6.0.0: + tough-cookie@6.0.1: dependencies: - tldts: 7.0.24 + tldts: 7.0.25 tr46@6.0.0: dependencies: @@ -6664,13 +6675,13 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 - typescript-eslint@8.47.0(eslint@9.39.3)(typescript@5.9.3): + typescript-eslint@8.47.0(eslint@9.39.4)(typescript@5.9.3): dependencies: - '@typescript-eslint/eslint-plugin': 8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.3)(typescript@5.9.3))(eslint@9.39.3)(typescript@5.9.3) - '@typescript-eslint/parser': 8.47.0(eslint@9.39.3)(typescript@5.9.3) + '@typescript-eslint/eslint-plugin': 8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.4)(typescript@5.9.3))(eslint@9.39.4)(typescript@5.9.3) + '@typescript-eslint/parser': 8.47.0(eslint@9.39.4)(typescript@5.9.3) '@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3) - '@typescript-eslint/utils': 8.47.0(eslint@9.39.3)(typescript@5.9.3) - eslint: 9.39.3 + '@typescript-eslint/utils': 8.47.0(eslint@9.39.4)(typescript@5.9.3) + eslint: 9.39.4 typescript: 5.9.3 transitivePeerDependencies: - supports-color @@ -6705,7 +6716,7 @@ snapshots: vary@1.1.2: {} - vite@7.3.1(@types/node@25.3.5)(sass@1.97.3): + vite@7.3.1(@types/node@25.5.0)(sass@1.97.3): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -6714,47 +6725,37 @@ snapshots: rollup: 4.59.0 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.3.5 + '@types/node': 25.5.0 fsevents: 2.3.3 sass: 1.97.3 - vitest@4.0.18(@types/node@25.3.5)(jsdom@27.4.0)(sass@1.97.3): + vitest@4.1.0(@types/node@25.5.0)(jsdom@27.4.0)(vite@7.3.1(@types/node@25.5.0)(sass@1.97.3)): dependencies: - '@vitest/expect': 4.0.18 - '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.3.5)(sass@1.97.3)) - '@vitest/pretty-format': 4.0.18 - '@vitest/runner': 4.0.18 - '@vitest/snapshot': 4.0.18 - '@vitest/spy': 4.0.18 - '@vitest/utils': 4.0.18 - es-module-lexer: 1.7.0 + '@vitest/expect': 4.1.0 + '@vitest/mocker': 4.1.0(vite@7.3.1(@types/node@25.5.0)(sass@1.97.3)) + '@vitest/pretty-format': 4.1.0 + '@vitest/runner': 4.1.0 + '@vitest/snapshot': 4.1.0 + '@vitest/spy': 4.1.0 + '@vitest/utils': 4.1.0 + es-module-lexer: 2.0.0 expect-type: 1.3.0 magic-string: 0.30.21 obug: 2.1.1 pathe: 2.0.3 picomatch: 4.0.3 - std-env: 3.10.0 + std-env: 4.0.0 tinybench: 2.9.0 - tinyexec: 1.0.2 + tinyexec: 1.0.4 tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 - vite: 7.3.1(@types/node@25.3.5)(sass@1.97.3) + tinyrainbow: 3.1.0 + vite: 7.3.1(@types/node@25.5.0)(sass@1.97.3) why-is-node-running: 2.3.0 optionalDependencies: - '@types/node': 25.3.5 + '@types/node': 25.5.0 jsdom: 27.4.0 transitivePeerDependencies: - - jiti - - less - - lightningcss - msw - - sass - - sass-embedded - - stylus - - sugarss - - terser - - tsx - - yaml w3c-xmlserializer@5.0.0: dependencies: diff --git a/uv.lock b/uv.lock index 080262a..f59c058 100644 --- a/uv.lock +++ b/uv.lock @@ -160,23 +160,23 @@ wheels = [ [[package]] name = "brotlicffi" -version = "1.2.0.0" +version = "1.2.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/84/85/57c314a6b35336efbbdc13e5fc9ae13f6b60a0647cfa7c1221178ac6d8ae/brotlicffi-1.2.0.0.tar.gz", hash = "sha256:34345d8d1f9d534fcac2249e57a4c3c8801a33c9942ff9f8574f67a175e17adb", size = 476682, upload-time = "2025-11-21T18:17:57.334Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/b6/017dc5f852ed9b8735af77774509271acbf1de02d238377667145fcee01d/brotlicffi-1.2.0.1.tar.gz", hash = "sha256:c20d5c596278307ad06414a6d95a892377ea274a5c6b790c2548c009385d621c", size = 478156, upload-time = "2026-03-05T19:54:11.547Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/87/ba6298c3d7f8d66ce80d7a487f2a487ebae74a79c6049c7c2990178ce529/brotlicffi-1.2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b13fb476a96f02e477a506423cb5e7bc21e0e3ac4c060c20ba31c44056e38c68", size = 433038, upload-time = "2026-03-05T17:57:37.96Z" }, - { url = "https://files.pythonhosted.org/packages/00/49/16c7a77d1cae0519953ef0389a11a9c2e2e62e87d04f8e7afbae40124255/brotlicffi-1.2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17db36fb581f7b951635cd6849553a95c6f2f53c1a707817d06eae5aeff5f6af", size = 1541124, upload-time = "2026-03-05T17:57:39.488Z" }, - { url = "https://files.pythonhosted.org/packages/e8/17/fab2c36ea820e2288f8c1bf562de1b6cd9f30e28d66f1ce2929a4baff6de/brotlicffi-1.2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:40190192790489a7b054312163d0ce82b07d1b6e706251036898ce1684ef12e9", size = 1541983, upload-time = "2026-03-05T17:57:41.061Z" }, - { url = "https://files.pythonhosted.org/packages/78/c9/849a669b3b3bb8ac96005cdef04df4db658c33443a7fc704a6d4a2f07a56/brotlicffi-1.2.0.0-cp314-cp314t-win32.whl", hash = "sha256:a8079e8ecc32ecef728036a1d9b7105991ce6a5385cf51ee8c02297c90fb08c2", size = 349046, upload-time = "2026-03-05T17:57:42.76Z" }, - { url = "https://files.pythonhosted.org/packages/a4/25/09c0fd21cfc451fa38ad538f4d18d8be566746531f7f27143f63f8c45a9f/brotlicffi-1.2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:ca90c4266704ca0a94de8f101b4ec029624273380574e4cf19301acfa46c61a0", size = 385653, upload-time = "2026-03-05T17:57:44.224Z" }, - { url = "https://files.pythonhosted.org/packages/e4/df/a72b284d8c7bef0ed5756b41c2eb7d0219a1dd6ac6762f1c7bdbc31ef3af/brotlicffi-1.2.0.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:9458d08a7ccde8e3c0afedbf2c70a8263227a68dea5ab13590593f4c0a4fd5f4", size = 432340, upload-time = "2025-11-21T18:17:42.277Z" }, - { url = "https://files.pythonhosted.org/packages/74/2b/cc55a2d1d6fb4f5d458fba44a3d3f91fb4320aa14145799fd3a996af0686/brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:84e3d0020cf1bd8b8131f4a07819edee9f283721566fe044a20ec792ca8fd8b7", size = 1534002, upload-time = "2025-11-21T18:17:43.746Z" }, - { url = "https://files.pythonhosted.org/packages/e4/9c/d51486bf366fc7d6735f0e46b5b96ca58dc005b250263525a1eea3cd5d21/brotlicffi-1.2.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:33cfb408d0cff64cd50bef268c0fed397c46fbb53944aa37264148614a62e990", size = 1536547, upload-time = "2025-11-21T18:17:45.729Z" }, - { url = "https://files.pythonhosted.org/packages/1b/37/293a9a0a7caf17e6e657668bebb92dfe730305999fe8c0e2703b8888789c/brotlicffi-1.2.0.0-cp38-abi3-win32.whl", hash = "sha256:23e5c912fdc6fd37143203820230374d24babd078fc054e18070a647118158f6", size = 343085, upload-time = "2025-11-21T18:17:48.887Z" }, - { url = "https://files.pythonhosted.org/packages/07/6b/6e92009df3b8b7272f85a0992b306b61c34b7ea1c4776643746e61c380ac/brotlicffi-1.2.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:f139a7cdfe4ae7859513067b736eb44d19fae1186f9e99370092f6915216451b", size = 378586, upload-time = "2025-11-21T18:17:50.531Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f9/dfa56316837fa798eac19358351e974de8e1e2ca9475af4cb90293cd6576/brotlicffi-1.2.0.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c85e65913cf2b79c57a3fdd05b98d9731d9255dc0cb696b09376cc091b9cddd", size = 433046, upload-time = "2026-03-05T19:53:46.209Z" }, + { url = "https://files.pythonhosted.org/packages/4a/f5/f8f492158c76b0d940388801f04f747028971ad5774287bded5f1e53f08d/brotlicffi-1.2.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:535f2d05d0273408abc13fc0eebb467afac17b0ad85090c8913690d40207dac5", size = 1541126, upload-time = "2026-03-05T19:53:48.248Z" }, + { url = "https://files.pythonhosted.org/packages/3b/e1/ff87af10ac419600c63e9287a0649c673673ae6b4f2bcf48e96cb2f89f60/brotlicffi-1.2.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce17eb798ca59ecec67a9bb3fd7a4304e120d1cd02953ce522d959b9a84d58ac", size = 1541983, upload-time = "2026-03-05T19:53:50.317Z" }, + { url = "https://files.pythonhosted.org/packages/47/c0/80ecd9bd45776109fab14040e478bf63e456967c9ddee2353d8330ed8de1/brotlicffi-1.2.0.1-cp314-cp314t-win32.whl", hash = "sha256:3c9544f83cb715d95d7eab3af4adbbef8b2093ad6382288a83b3a25feb1a57ec", size = 349047, upload-time = "2026-03-05T19:53:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/ab/98/13e5b250236a281b6cd9e92a01ee1ae231029fa78faee932ef3766e1cb24/brotlicffi-1.2.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:625f8115d32ae9c0740d01ea51518437c3fbaa3e78d41cb18459f6f7ac326000", size = 385652, upload-time = "2026-03-05T19:53:53.892Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9f/b98dcd4af47994cee97aebac866996a006a2e5fc1fd1e2b82a8ad95cf09c/brotlicffi-1.2.0.1-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:91ba5f0ccc040f6ff8f7efaf839f797723d03ed46acb8ae9408f99ffd2572cf4", size = 432608, upload-time = "2026-03-05T19:53:56.736Z" }, + { url = "https://files.pythonhosted.org/packages/b1/7a/ac4ee56595a061e3718a6d1ea7e921f4df156894acffb28ed88a1fd52022/brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be9a670c6811af30a4bd42d7116dc5895d3b41beaa8ed8a89050447a0181f5ce", size = 1534257, upload-time = "2026-03-05T19:53:58.667Z" }, + { url = "https://files.pythonhosted.org/packages/99/39/e7410db7f6f56de57744ea52a115084ceb2735f4d44973f349bb92136586/brotlicffi-1.2.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f3314a3476f59e5443f9f72a6dff16edc0c3463c9b318feaef04ae3e4683f5a", size = 1536838, upload-time = "2026-03-05T19:54:00.705Z" }, + { url = "https://files.pythonhosted.org/packages/a6/75/6e7977d1935fc3fbb201cbd619be8f2c7aea25d40a096967132854b34708/brotlicffi-1.2.0.1-cp38-abi3-win32.whl", hash = "sha256:82ea52e2b5d3145b6c406ebd3efb0d55db718b7ad996bd70c62cec0439de1187", size = 343337, upload-time = "2026-03-05T19:54:02.446Z" }, + { url = "https://files.pythonhosted.org/packages/d8/ef/e7e485ce5e4ba3843a0a92feb767c7b6098fd6e65ce752918074d175ae71/brotlicffi-1.2.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:da2e82a08e7778b8bc539d27ca03cdd684113e81394bfaaad8d0dfc6a17ddede", size = 379026, upload-time = "2026-03-05T19:54:04.322Z" }, ] [[package]] @@ -235,43 +235,43 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.4" +version = "3.4.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/35/02daf95b9cd686320bb622eb148792655c9412dbb9b67abb5694e5910a24/charset_normalizer-3.4.5.tar.gz", hash = "sha256:95adae7b6c42a6c5b5b559b1a99149f090a57128155daeea91732c8d970d8644", size = 134804, upload-time = "2026-03-06T06:03:19.46Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" }, - { url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" }, - { url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, - { url = "https://files.pythonhosted.org/packages/91/ed/9706e4070682d1cc219050b6048bfd293ccf67b3d4f5a4f39207453d4b99/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328", size = 161346, upload-time = "2025-10-14T04:41:16.738Z" }, - { url = "https://files.pythonhosted.org/packages/d5/0d/031f0d95e4972901a2f6f09ef055751805ff541511dc1252ba3ca1f80cf5/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede", size = 158874, upload-time = "2025-10-14T04:41:17.923Z" }, - { url = "https://files.pythonhosted.org/packages/f5/83/6ab5883f57c9c801ce5e5677242328aa45592be8a00644310a008d04f922/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894", size = 153076, upload-time = "2025-10-14T04:41:19.106Z" }, - { url = "https://files.pythonhosted.org/packages/75/1e/5ff781ddf5260e387d6419959ee89ef13878229732732ee73cdae01800f2/charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1", size = 150601, upload-time = "2025-10-14T04:41:20.245Z" }, - { url = "https://files.pythonhosted.org/packages/d7/57/71be810965493d3510a6ca79b90c19e48696fb1ff964da319334b12677f0/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490", size = 150376, upload-time = "2025-10-14T04:41:21.398Z" }, - { url = "https://files.pythonhosted.org/packages/e5/d5/c3d057a78c181d007014feb7e9f2e65905a6c4ef182c0ddf0de2924edd65/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44", size = 144825, upload-time = "2025-10-14T04:41:22.583Z" }, - { url = "https://files.pythonhosted.org/packages/e6/8c/d0406294828d4976f275ffbe66f00266c4b3136b7506941d87c00cab5272/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133", size = 162583, upload-time = "2025-10-14T04:41:23.754Z" }, - { url = "https://files.pythonhosted.org/packages/d7/24/e2aa1f18c8f15c4c0e932d9287b8609dd30ad56dbe41d926bd846e22fb8d/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3", size = 150366, upload-time = "2025-10-14T04:41:25.27Z" }, - { url = "https://files.pythonhosted.org/packages/e4/5b/1e6160c7739aad1e2df054300cc618b06bf784a7a164b0f238360721ab86/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e", size = 160300, upload-time = "2025-10-14T04:41:26.725Z" }, - { url = "https://files.pythonhosted.org/packages/7a/10/f882167cd207fbdd743e55534d5d9620e095089d176d55cb22d5322f2afd/charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc", size = 154465, upload-time = "2025-10-14T04:41:28.322Z" }, - { url = "https://files.pythonhosted.org/packages/89/66/c7a9e1b7429be72123441bfdbaf2bc13faab3f90b933f664db506dea5915/charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac", size = 99404, upload-time = "2025-10-14T04:41:29.95Z" }, - { url = "https://files.pythonhosted.org/packages/c4/26/b9924fa27db384bdcd97ab83b4f0a8058d96ad9626ead570674d5e737d90/charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14", size = 107092, upload-time = "2025-10-14T04:41:31.188Z" }, - { url = "https://files.pythonhosted.org/packages/af/8f/3ed4bfa0c0c72a7ca17f0380cd9e4dd842b09f664e780c13cff1dcf2ef1b/charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2", size = 100408, upload-time = "2025-10-14T04:41:32.624Z" }, - { url = "https://files.pythonhosted.org/packages/2a/35/7051599bd493e62411d6ede36fd5af83a38f37c4767b92884df7301db25d/charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd", size = 207746, upload-time = "2025-10-14T04:41:33.773Z" }, - { url = "https://files.pythonhosted.org/packages/10/9a/97c8d48ef10d6cd4fcead2415523221624bf58bcf68a802721a6bc807c8f/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb", size = 147889, upload-time = "2025-10-14T04:41:34.897Z" }, - { url = "https://files.pythonhosted.org/packages/10/bf/979224a919a1b606c82bd2c5fa49b5c6d5727aa47b4312bb27b1734f53cd/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e", size = 143641, upload-time = "2025-10-14T04:41:36.116Z" }, - { url = "https://files.pythonhosted.org/packages/ba/33/0ad65587441fc730dc7bd90e9716b30b4702dc7b617e6ba4997dc8651495/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14", size = 160779, upload-time = "2025-10-14T04:41:37.229Z" }, - { url = "https://files.pythonhosted.org/packages/67/ed/331d6b249259ee71ddea93f6f2f0a56cfebd46938bde6fcc6f7b9a3d0e09/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191", size = 159035, upload-time = "2025-10-14T04:41:38.368Z" }, - { url = "https://files.pythonhosted.org/packages/67/ff/f6b948ca32e4f2a4576aa129d8bed61f2e0543bf9f5f2b7fc3758ed005c9/charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838", size = 152542, upload-time = "2025-10-14T04:41:39.862Z" }, - { url = "https://files.pythonhosted.org/packages/16/85/276033dcbcc369eb176594de22728541a925b2632f9716428c851b149e83/charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6", size = 149524, upload-time = "2025-10-14T04:41:41.319Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f2/6a2a1f722b6aba37050e626530a46a68f74e63683947a8acff92569f979a/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e", size = 150395, upload-time = "2025-10-14T04:41:42.539Z" }, - { url = "https://files.pythonhosted.org/packages/60/bb/2186cb2f2bbaea6338cad15ce23a67f9b0672929744381e28b0592676824/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c", size = 143680, upload-time = "2025-10-14T04:41:43.661Z" }, - { url = "https://files.pythonhosted.org/packages/7d/a5/bf6f13b772fbb2a90360eb620d52ed8f796f3c5caee8398c3b2eb7b1c60d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090", size = 162045, upload-time = "2025-10-14T04:41:44.821Z" }, - { url = "https://files.pythonhosted.org/packages/df/c5/d1be898bf0dc3ef9030c3825e5d3b83f2c528d207d246cbabe245966808d/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152", size = 149687, upload-time = "2025-10-14T04:41:46.442Z" }, - { url = "https://files.pythonhosted.org/packages/a5/42/90c1f7b9341eef50c8a1cb3f098ac43b0508413f33affd762855f67a410e/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828", size = 160014, upload-time = "2025-10-14T04:41:47.631Z" }, - { url = "https://files.pythonhosted.org/packages/76/be/4d3ee471e8145d12795ab655ece37baed0929462a86e72372fd25859047c/charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec", size = 154044, upload-time = "2025-10-14T04:41:48.81Z" }, - { url = "https://files.pythonhosted.org/packages/b0/6f/8f7af07237c34a1defe7defc565a9bc1807762f672c0fde711a4b22bf9c0/charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9", size = 99940, upload-time = "2025-10-14T04:41:49.946Z" }, - { url = "https://files.pythonhosted.org/packages/4b/51/8ade005e5ca5b0d80fb4aff72a3775b325bdc3d27408c8113811a7cbe640/charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c", size = 107104, upload-time = "2025-10-14T04:41:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/da/5f/6b8f83a55bb8278772c5ae54a577f3099025f9ade59d0136ac24a0df4bde/charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2", size = 100743, upload-time = "2025-10-14T04:41:52.122Z" }, - { url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" }, + { url = "https://files.pythonhosted.org/packages/f5/48/9f34ec4bb24aa3fdba1890c1bddb97c8a4be1bd84ef5c42ac2352563ad05/charset_normalizer-3.4.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ac59c15e3f1465f722607800c68713f9fbc2f672b9eb649fe831da4019ae9b23", size = 280788, upload-time = "2026-03-06T06:01:37.126Z" }, + { url = "https://files.pythonhosted.org/packages/0e/09/6003e7ffeb90cc0560da893e3208396a44c210c5ee42efff539639def59b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:165c7b21d19365464e8f70e5ce5e12524c58b48c78c1f5a57524603c1ab003f8", size = 188890, upload-time = "2026-03-06T06:01:38.73Z" }, + { url = "https://files.pythonhosted.org/packages/42/1e/02706edf19e390680daa694d17e2b8eab4b5f7ac285e2a51168b4b22ee6b/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:28269983f25a4da0425743d0d257a2d6921ea7d9b83599d4039486ec5b9f911d", size = 206136, upload-time = "2026-03-06T06:01:40.016Z" }, + { url = "https://files.pythonhosted.org/packages/c7/87/942c3def1b37baf3cf786bad01249190f3ca3d5e63a84f831e704977de1f/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d27ce22ec453564770d29d03a9506d449efbb9fa13c00842262b2f6801c48cce", size = 202551, upload-time = "2026-03-06T06:01:41.522Z" }, + { url = "https://files.pythonhosted.org/packages/94/0a/af49691938dfe175d71b8a929bd7e4ace2809c0c5134e28bc535660d5262/charset_normalizer-3.4.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0625665e4ebdddb553ab185de5db7054393af8879fb0c87bd5690d14379d6819", size = 195572, upload-time = "2026-03-06T06:01:43.208Z" }, + { url = "https://files.pythonhosted.org/packages/20/ea/dfb1792a8050a8e694cfbde1570ff97ff74e48afd874152d38163d1df9ae/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:c23eb3263356d94858655b3e63f85ac5d50970c6e8febcdde7830209139cc37d", size = 184438, upload-time = "2026-03-06T06:01:44.755Z" }, + { url = "https://files.pythonhosted.org/packages/72/12/c281e2067466e3ddd0595bfaea58a6946765ace5c72dfa3edc2f5f118026/charset_normalizer-3.4.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e6302ca4ae283deb0af68d2fbf467474b8b6aedcd3dab4db187e07f94c109763", size = 193035, upload-time = "2026-03-06T06:01:46.051Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4f/3792c056e7708e10464bad0438a44708886fb8f92e3c3d29ec5e2d964d42/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e51ae7d81c825761d941962450f50d041db028b7278e7b08930b4541b3e45cb9", size = 191340, upload-time = "2026-03-06T06:01:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/e7/86/80ddba897127b5c7a9bccc481b0cd36c8fefa485d113262f0fe4332f0bf4/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:597d10dec876923e5c59e48dbd366e852eacb2b806029491d307daea6b917d7c", size = 185464, upload-time = "2026-03-06T06:01:48.764Z" }, + { url = "https://files.pythonhosted.org/packages/4d/00/b5eff85ba198faacab83e0e4b6f0648155f072278e3b392a82478f8b988b/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5cffde4032a197bd3b42fd0b9509ec60fb70918d6970e4cc773f20fc9180ca67", size = 208014, upload-time = "2026-03-06T06:01:50.371Z" }, + { url = "https://files.pythonhosted.org/packages/c8/11/d36f70be01597fd30850dde8a1269ebc8efadd23ba5785808454f2389bde/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2da4eedcb6338e2321e831a0165759c0c620e37f8cd044a263ff67493be8ffb3", size = 193297, upload-time = "2026-03-06T06:01:51.933Z" }, + { url = "https://files.pythonhosted.org/packages/1a/1d/259eb0a53d4910536c7c2abb9cb25f4153548efb42800c6a9456764649c0/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:65a126fb4b070d05340a84fc709dd9e7c75d9b063b610ece8a60197a291d0adf", size = 204321, upload-time = "2026-03-06T06:01:53.887Z" }, + { url = "https://files.pythonhosted.org/packages/84/31/faa6c5b9d3688715e1ed1bb9d124c384fe2fc1633a409e503ffe1c6398c1/charset_normalizer-3.4.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c7a80a9242963416bd81f99349d5f3fce1843c303bd404f204918b6d75a75fd6", size = 197509, upload-time = "2026-03-06T06:01:56.439Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a5/c7d9dd1503ffc08950b3260f5d39ec2366dd08254f0900ecbcf3a6197c7c/charset_normalizer-3.4.5-cp313-cp313-win32.whl", hash = "sha256:f1d725b754e967e648046f00c4facc42d414840f5ccc670c5670f59f83693e4f", size = 132284, upload-time = "2026-03-06T06:01:57.812Z" }, + { url = "https://files.pythonhosted.org/packages/b9/0f/57072b253af40c8aa6636e6de7d75985624c1eb392815b2f934199340a89/charset_normalizer-3.4.5-cp313-cp313-win_amd64.whl", hash = "sha256:e37bd100d2c5d3ba35db9c7c5ba5a9228cbcffe5c4778dc824b164e5257813d7", size = 142630, upload-time = "2026-03-06T06:01:59.062Z" }, + { url = "https://files.pythonhosted.org/packages/31/41/1c4b7cc9f13bd9d369ce3bc993e13d374ce25fa38a2663644283ecf422c1/charset_normalizer-3.4.5-cp313-cp313-win_arm64.whl", hash = "sha256:93b3b2cc5cf1b8743660ce77a4f45f3f6d1172068207c1defc779a36eea6bb36", size = 133254, upload-time = "2026-03-06T06:02:00.281Z" }, + { url = "https://files.pythonhosted.org/packages/43/be/0f0fd9bb4a7fa4fb5067fb7d9ac693d4e928d306f80a0d02bde43a7c4aee/charset_normalizer-3.4.5-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8197abe5ca1ffb7d91e78360f915eef5addff270f8a71c1fc5be24a56f3e4873", size = 280232, upload-time = "2026-03-06T06:02:01.508Z" }, + { url = "https://files.pythonhosted.org/packages/28/02/983b5445e4bef49cd8c9da73a8e029f0825f39b74a06d201bfaa2e55142a/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a2aecdb364b8a1802afdc7f9327d55dad5366bc97d8502d0f5854e50712dbc5f", size = 189688, upload-time = "2026-03-06T06:02:02.857Z" }, + { url = "https://files.pythonhosted.org/packages/d0/88/152745c5166437687028027dc080e2daed6fe11cfa95a22f4602591c42db/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a66aa5022bf81ab4b1bebfb009db4fd68e0c6d4307a1ce5ef6a26e5878dfc9e4", size = 206833, upload-time = "2026-03-06T06:02:05.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0f/ebc15c8b02af2f19be9678d6eed115feeeccc45ce1f4b098d986c13e8769/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d77f97e515688bd615c1d1f795d540f32542d514242067adcb8ef532504cb9ee", size = 202879, upload-time = "2026-03-06T06:02:06.446Z" }, + { url = "https://files.pythonhosted.org/packages/38/9c/71336bff6934418dc8d1e8a1644176ac9088068bc571da612767619c97b3/charset_normalizer-3.4.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01a1ed54b953303ca7e310fafe0fe347aab348bd81834a0bcd602eb538f89d66", size = 195764, upload-time = "2026-03-06T06:02:08.763Z" }, + { url = "https://files.pythonhosted.org/packages/b7/95/ce92fde4f98615661871bc282a856cf9b8a15f686ba0af012984660d480b/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:b2d37d78297b39a9eb9eb92c0f6df98c706467282055419df141389b23f93362", size = 183728, upload-time = "2026-03-06T06:02:10.137Z" }, + { url = "https://files.pythonhosted.org/packages/1c/e7/f5b4588d94e747ce45ae680f0f242bc2d98dbd4eccfab73e6160b6893893/charset_normalizer-3.4.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e71bbb595973622b817c042bd943c3f3667e9c9983ce3d205f973f486fec98a7", size = 192937, upload-time = "2026-03-06T06:02:11.663Z" }, + { url = "https://files.pythonhosted.org/packages/f9/29/9d94ed6b929bf9f48bf6ede6e7474576499f07c4c5e878fb186083622716/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cd966c2559f501c6fd69294d082c2934c8dd4719deb32c22961a5ac6db0df1d", size = 192040, upload-time = "2026-03-06T06:02:13.489Z" }, + { url = "https://files.pythonhosted.org/packages/15/d2/1a093a1cf827957f9445f2fe7298bcc16f8fc5e05c1ed2ad1af0b239035e/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:d5e52d127045d6ae01a1e821acfad2f3a1866c54d0e837828538fabe8d9d1bd6", size = 184107, upload-time = "2026-03-06T06:02:14.83Z" }, + { url = "https://files.pythonhosted.org/packages/0f/7d/82068ce16bd36135df7b97f6333c5d808b94e01d4599a682e2337ed5fd14/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:30a2b1a48478c3428d047ed9690d57c23038dac838a87ad624c85c0a78ebeb39", size = 208310, upload-time = "2026-03-06T06:02:16.165Z" }, + { url = "https://files.pythonhosted.org/packages/84/4e/4dfb52307bb6af4a5c9e73e482d171b81d36f522b21ccd28a49656baa680/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d8ed79b8f6372ca4254955005830fd61c1ccdd8c0fac6603e2c145c61dd95db6", size = 192918, upload-time = "2026-03-06T06:02:18.144Z" }, + { url = "https://files.pythonhosted.org/packages/08/a4/159ff7da662cf7201502ca89980b8f06acf3e887b278956646a8aeb178ab/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c5af897b45fa606b12464ccbe0014bbf8c09191e0a66aab6aa9d5cf6e77e0c94", size = 204615, upload-time = "2026-03-06T06:02:19.821Z" }, + { url = "https://files.pythonhosted.org/packages/d6/62/0dd6172203cb6b429ffffc9935001fde42e5250d57f07b0c28c6046deb6b/charset_normalizer-3.4.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1088345bcc93c58d8d8f3d783eca4a6e7a7752bbff26c3eee7e73c597c191c2e", size = 197784, upload-time = "2026-03-06T06:02:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/c7/5e/1aab5cb737039b9c59e63627dc8bbc0d02562a14f831cc450e5f91d84ce1/charset_normalizer-3.4.5-cp314-cp314-win32.whl", hash = "sha256:ee57b926940ba00bca7ba7041e665cc956e55ef482f851b9b65acb20d867e7a2", size = 133009, upload-time = "2026-03-06T06:02:23.289Z" }, + { url = "https://files.pythonhosted.org/packages/40/65/e7c6c77d7aaa4c0d7974f2e403e17f0ed2cb0fc135f77d686b916bf1eead/charset_normalizer-3.4.5-cp314-cp314-win_amd64.whl", hash = "sha256:4481e6da1830c8a1cc0b746b47f603b653dadb690bcd851d039ffaefe70533aa", size = 143511, upload-time = "2026-03-06T06:02:26.195Z" }, + { url = "https://files.pythonhosted.org/packages/ba/91/52b0841c71f152f563b8e072896c14e3d83b195c188b338d3cc2e582d1d4/charset_normalizer-3.4.5-cp314-cp314-win_arm64.whl", hash = "sha256:97ab7787092eb9b50fb47fa04f24c75b768a606af1bcba1957f07f128a7219e4", size = 133775, upload-time = "2026-03-06T06:02:27.473Z" }, + { url = "https://files.pythonhosted.org/packages/c5/60/3a621758945513adfd4db86827a5bafcc615f913dbd0b4c2ed64a65731be/charset_normalizer-3.4.5-py3-none-any.whl", hash = "sha256:9db5e3fcdcee89a78c04dffb3fe33c79f77bd741a624946db2591c81b2fc85b0", size = 55455, upload-time = "2026-03-06T06:03:17.827Z" }, ] [[package]] @@ -308,15 +308,15 @@ wheels = [ [[package]] name = "deno" -version = "2.7.2" +version = "2.7.5" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/bd/29/b2941d53d94094e20e52def86956528140dbe60b49d715803f7e9799d42f/deno-2.7.2.tar.gz", hash = "sha256:3dc9461ac4dd0d6661769f03460861709e17c4e516dfce14676e6a3146824b7b", size = 8167, upload-time = "2026-03-03T16:10:51.429Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/31/8bbaf3fb6a41929ae161be0b2a79b2747b5e5490811573ef60af7e3aeac3/deno-2.7.5.tar.gz", hash = "sha256:50635e0462697fa6e79d90bcacbe98e19f785e604c0e5061754de89b3668af83", size = 8166, upload-time = "2026-03-11T12:48:44.286Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0c/a0/9e6f45c25ef36db827e75bd35bf9378c196a6bed2804a8259d1d63bab84f/deno-2.7.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:67509919fa9df639d9375e441648ae5a3ab9bb1ce6fcddc21c49c08368af4d68", size = 46325714, upload-time = "2026-03-03T16:10:35.82Z" }, - { url = "https://files.pythonhosted.org/packages/83/ce/085c3002cdfc0d33b30896b3d1469024c23e3971cba4a15ae3983c48d2e4/deno-2.7.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:a19f75d7a148a2d030543db88734f03648e31dc7385a9c62aa1d975e2b0df8d9", size = 43264279, upload-time = "2026-03-03T16:10:39.011Z" }, - { url = "https://files.pythonhosted.org/packages/38/f0/c415c08ca30fb084887a96b88df7f6511c98575b365db87b0fac76a82773/deno-2.7.2-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:f7b63f13c9fdeb18d0435e80aa4677878ac1b9ac23a49c7570958b9d81772e06", size = 47024484, upload-time = "2026-03-03T16:10:42.619Z" }, - { url = "https://files.pythonhosted.org/packages/e6/14/bfac1928082f78f120aaff7608f211a8beab8f66e72defc0ac85d6f52f84/deno-2.7.2-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:bded39ebc9d19748a13a4c046a715f12c445a3e15c0b4cde6d42cc47793efcf0", size = 48981918, upload-time = "2026-03-03T16:10:45.822Z" }, - { url = "https://files.pythonhosted.org/packages/79/07/b332f98969937d435ba2905195a0b3dd2162f192659595dde88c615b04e1/deno-2.7.2-py3-none-win_amd64.whl", hash = "sha256:5d525d270e16d5ea22ad90a65e1ebc0dff8b83068d698f6bad138bfa857e4d28", size = 48330774, upload-time = "2026-03-03T16:10:49.209Z" }, + { url = "https://files.pythonhosted.org/packages/68/15/47c4b8da4e1b312ab14a2517e3f484c4d67a879cb5099cb6c33b8ce00c8c/deno-2.7.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:29cb89cdaea5f36133841fb4da058b1c6cb70d117ebfc7a24c717747b58e8503", size = 46641593, upload-time = "2026-03-11T12:48:16.589Z" }, + { url = "https://files.pythonhosted.org/packages/1c/3a/c3f8842b7499ff3faeb7508711a82b736d3a4c6e0ffb359191386bcf539d/deno-2.7.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:6456980341e97e4eb88e0c560fa57cd1b5f732e0eaadccc6c47d5ada73a71ff3", size = 43537874, upload-time = "2026-03-11T12:48:21.958Z" }, + { url = "https://files.pythonhosted.org/packages/71/a2/53a013ba3509648582748678d5c6980210a45e0913934f91bfe1ec237e07/deno-2.7.5-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:fdc1e647a06ef792643237c030f45295692b0abc05d5bc9894fb11fd70876953", size = 47265090, upload-time = "2026-03-11T12:48:26.819Z" }, + { url = "https://files.pythonhosted.org/packages/3e/85/88c76daa72575f7229bb94191f15f4771f0614227bf8467bfe06e051f4ab/deno-2.7.5-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:c15e6b8ccf5f0808cd5ba243ea4eea7d8d78f6fdff228f5c6c85b96ba286bd3c", size = 49262188, upload-time = "2026-03-11T12:48:32.125Z" }, + { url = "https://files.pythonhosted.org/packages/42/5e/501a92ef93d6d46ed8a1a8c03cff8bcbccbc06c1f59b163113ff09cd23cf/deno-2.7.5-py3-none-win_amd64.whl", hash = "sha256:3e3d06006ee39901dd23068c4a501a4a524fb71c323e22503b1b2ddf236da463", size = 48481169, upload-time = "2026-03-11T12:48:38.684Z" }, ] [[package]] @@ -560,11 +560,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.9.2" +version = "4.9.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1b/04/fea538adf7dbbd6d186f551d595961e564a3b6715bdf276b477460858672/platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291", size = 28394, upload-time = "2026-02-16T03:56:10.574Z" } +sdist = { url = "https://files.pythonhosted.org/packages/19/56/8d4c30c8a1d07013911a8fdbd8f89440ef9f08d07a1b50ab8ca8be5a20f9/platformdirs-4.9.4.tar.gz", hash = "sha256:1ec356301b7dc906d83f371c8f487070e99d3ccf9e501686456394622a01a934", size = 28737, upload-time = "2026-03-05T18:34:13.271Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/48/31/05e764397056194206169869b50cf2fee4dbbbc71b344705b9c0d878d4d8/platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd", size = 21168, upload-time = "2026-02-16T03:56:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/63/d7/97f7e3a6abb67d8080dd406fd4df842c2be0efaf712d1c899c32a075027c/platformdirs-4.9.4-py3-none-any.whl", hash = "sha256:68a9a4619a666ea6439f2ff250c12a853cd1cbd5158d258bd824a7df6be2f868", size = 21216, upload-time = "2026-03-05T18:34:12.172Z" }, ] [[package]] From 7fa1fc7938187e678fdc6f33d572fb1011b8ddfb Mon Sep 17 00:00:00 2001 From: Alex Shnitman Date: Sun, 15 Mar 2026 20:53:13 +0200 Subject: [PATCH 013/107] code review fixes --- .github/workflows/main.yml | 39 ++- Dockerfile | 7 +- app/dl_formats.py | 12 +- app/main.py | 102 ++++-- app/tests/test_dl_formats.py | 21 ++ app/ytdl.py | 43 ++- ui/angular.json | 4 +- ui/src/app/app.config.ts | 2 +- ui/src/app/app.html | 52 +-- ui/src/app/app.sass | 67 +--- ui/src/app/app.ts | 319 +++++++++++------- ui/src/app/components/index.ts | 4 +- .../components/master-checkbox.component.ts | 8 +- .../components/slave-checkbox.component.ts | 12 +- ui/src/app/pipes/speed.pipe.spec.ts | 15 + ui/src/app/pipes/speed.pipe.ts | 38 +-- ui/src/app/services/downloads.service.ts | 142 +++----- ui/src/app/services/index.ts | 1 - ui/src/app/services/speed.service.ts | 39 --- ui/src/styles.sass | 19 ++ 20 files changed, 498 insertions(+), 448 deletions(-) create mode 100644 app/tests/test_dl_formats.py create mode 100644 ui/src/app/pipes/speed.pipe.spec.ts delete mode 100644 ui/src/app/services/speed.service.ts diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5e4a8aa..dd4a57c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -6,13 +6,50 @@ on: - 'master' jobs: + quality-checks: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: lts/* + - name: Enable pnpm + run: corepack enable + - name: Install frontend dependencies + working-directory: ui + run: pnpm install --frozen-lockfile + - name: Run frontend lint + working-directory: ui + run: pnpm run lint + - name: Build frontend + working-directory: ui + run: pnpm run build + - name: Set up Python + uses: actions/setup-python@v6 + with: + python-version: '3.13' + - name: Run backend smoke checks + run: python -m compileall app + - name: Run backend tests + run: python -m unittest discover -s app/tests -p "test_*.py" + - name: Run Trivy filesystem scan + uses: aquasecurity/trivy-action@0.33.1 + with: + scan-type: fs + scan-ref: . + format: table + severity: CRITICAL,HIGH + dockerhub-build-push: + needs: quality-checks runs-on: ubuntu-latest steps: - name: Get current date id: date - run: echo "::set-output name=date::$(date +'%Y.%m.%d')" + run: echo "date=$(date +'%Y.%m.%d')" >> "$GITHUB_OUTPUT" - name: Checkout uses: actions/checkout@v6 diff --git a/Dockerfile b/Dockerfile index 4dff7c0..cad8d56 100644 --- a/Dockerfile +++ b/Dockerfile @@ -63,11 +63,12 @@ ENV PUID=1000 ENV PGID=1000 ENV UMASK=022 -ENV DOWNLOAD_DIR /downloads -ENV STATE_DIR /downloads/.metube -ENV TEMP_DIR /downloads +ENV DOWNLOAD_DIR=/downloads +ENV STATE_DIR=/downloads/.metube +ENV TEMP_DIR=/downloads VOLUME /downloads EXPOSE 8081 +HEALTHCHECK --interval=30s --timeout=5s --start-period=20s --retries=3 CMD curl -fsS "http://localhost:8081/" || exit 1 # Add build-time argument for version ARG VERSION=dev diff --git a/app/dl_formats.py b/app/dl_formats.py index e229366..7933451 100644 --- a/app/dl_formats.py +++ b/app/dl_formats.py @@ -53,12 +53,12 @@ def get_format(download_type: str, codec: str, format: str, quality: str) -> str if download_type == "audio": if format not in AUDIO_FORMATS: - raise Exception(f"Unknown audio format {format}") + raise ValueError(f"Unknown audio format {format}") return f"bestaudio[ext={format}]/bestaudio/best" if download_type == "video": if format not in ("any", "mp4", "ios"): - raise Exception(f"Unknown video format {format}") + raise ValueError(f"Unknown video format {format}") vfmt, afmt = ("[ext=mp4]", "[ext=m4a]") if format in ("mp4", "ios") else ("", "") vres = f"[height<={quality}]" if quality not in ("best", "worst") else "" vcombo = vres + vfmt @@ -71,12 +71,12 @@ def get_format(download_type: str, codec: str, format: str, quality: str) -> str return f"bestvideo{codec_filter}{vcombo}+bestaudio{afmt}/bestvideo{vcombo}+bestaudio{afmt}/best{vcombo}" return f"bestvideo{vcombo}+bestaudio{afmt}/best{vcombo}" - raise Exception(f"Unknown download_type {download_type}") + raise ValueError(f"Unknown download_type {download_type}") def get_opts( download_type: str, - codec: str, + _codec: str, format: str, quality: str, ytdl_opts: dict, @@ -96,8 +96,6 @@ def get_opts( Returns: dict: extended options """ - del codec # kept for parity with get_format signature - download_type = (download_type or "video").strip().lower() format = (format or "any").strip().lower() opts = copy.deepcopy(ytdl_opts) @@ -113,7 +111,7 @@ def get_opts( } ) - if format not in ("wav") and "writethumbnail" not in opts: + if format != "wav" and "writethumbnail" not in opts: opts["writethumbnail"] = True postprocessors.append( { diff --git a/app/main.py b/app/main.py index 6e3cbbf..ff31190 100644 --- a/app/main.py +++ b/app/main.py @@ -16,25 +16,15 @@ import pathlib import re from watchfiles import DefaultFilter, Change, awatch -from ytdl import DownloadQueueNotifier, DownloadQueue +from ytdl import DownloadQueueNotifier, DownloadQueue, Download from yt_dlp.version import __version__ as yt_dlp_version log = logging.getLogger('main') def parseLogLevel(logLevel): - match logLevel: - case 'DEBUG': - return logging.DEBUG - case 'INFO': - return logging.INFO - case 'WARNING': - return logging.WARNING - case 'ERROR': - return logging.ERROR - case 'CRITICAL': - return logging.CRITICAL - case _: - return None + if not isinstance(logLevel, str): + return None + return getattr(logging, logLevel.upper(), None) # Configure logging before Config() uses it so early messages are not dropped. # Only configure if no handlers are set (avoid clobbering hosting app settings). @@ -71,7 +61,7 @@ class Config: 'KEYFILE': '', 'BASE_DIR': '', 'DEFAULT_THEME': 'auto', - 'MAX_CONCURRENT_DOWNLOADS': 3, + 'MAX_CONCURRENT_DOWNLOADS': '3', 'LOGLEVEL': 'INFO', 'ENABLE_ACCESSLOG': 'false', } @@ -181,7 +171,7 @@ class ObjectSerializer(json.JSONEncoder): elif hasattr(obj, '__iter__') and not isinstance(obj, (str, bytes)): try: return list(obj) - except: + except Exception: pass # Fall back to default behavior return json.JSONEncoder.default(self, obj) @@ -280,6 +270,7 @@ class Notifier(DownloadQueueNotifier): dqueue = DownloadQueue(config, Notifier()) app.on_startup.append(lambda app: dqueue.initialize()) +app.on_cleanup.append(lambda app: Download.shutdown_manager()) class FileOpsFilter(DefaultFilter): def __call__(self, change_type: int, path: str) -> bool: @@ -330,12 +321,30 @@ async def watch_files(): if config.YTDL_OPTIONS_FILE: app.on_startup.append(lambda app: watch_files()) + +async def _read_json_request(request: web.Request) -> dict: + try: + post = await request.json() + except json.JSONDecodeError as exc: + raise web.HTTPBadRequest(reason='Invalid JSON request body') from exc + if not isinstance(post, dict): + raise web.HTTPBadRequest(reason='JSON request body must be an object') + return post + + @routes.post(config.URL_PREFIX + 'add') async def add(request): log.info("Received request to add download") - post = await request.json() + post = await _read_json_request(request) post = _migrate_legacy_request(post) - log.info(f"Request data: {post}") + log.info( + "Add download request: type=%s quality=%s format=%s has_folder=%s auto_start=%s", + post.get('download_type'), + post.get('quality'), + post.get('format'), + bool(post.get('folder')), + post.get('auto_start'), + ) url = post.get('url') download_type = post.get('download_type') codec = post.get('codec') @@ -415,7 +424,10 @@ async def add(request): quality = 'best' codec = 'auto' - playlist_item_limit = int(playlist_item_limit) + try: + playlist_item_limit = int(playlist_item_limit) + except (TypeError, ValueError) as exc: + raise web.HTTPBadRequest(reason='playlist_item_limit must be an integer') from exc status = await dqueue.add( url, @@ -441,7 +453,7 @@ async def cancel_add(request): @routes.post(config.URL_PREFIX + 'delete') async def delete(request): - post = await request.json() + post = await _read_json_request(request) ids = post.get('ids') where = post.get('where') if not ids or where not in ['queue', 'done']: @@ -453,7 +465,7 @@ async def delete(request): @routes.post(config.URL_PREFIX + 'start') async def start(request): - post = await request.json() + post = await _read_json_request(request) ids = post.get('ids') log.info(f"Received request to start pending downloads for ids: {ids}") status = await dqueue.start_pending(ids) @@ -468,17 +480,23 @@ async def upload_cookies(request): field = await reader.next() if field is None or field.name != 'cookies': return web.Response(status=400, text=serializer.encode({'status': 'error', 'msg': 'No cookies file provided'})) + + max_size = 1_000_000 # 1MB limit size = 0 - with open(COOKIES_PATH, 'wb') as f: - while True: - chunk = await field.read_chunk() - if not chunk: - break - size += len(chunk) - if size > 1_000_000: # 1MB limit - os.remove(COOKIES_PATH) - return web.Response(status=400, text=serializer.encode({'status': 'error', 'msg': 'Cookie file too large (max 1MB)'})) - f.write(chunk) + content = bytearray() + while True: + chunk = await field.read_chunk() + if not chunk: + break + size += len(chunk) + if size > max_size: + return web.Response(status=400, text=serializer.encode({'status': 'error', 'msg': 'Cookie file too large (max 1MB)'})) + content.extend(chunk) + + tmp_cookie_path = f"{COOKIES_PATH}.tmp" + with open(tmp_cookie_path, 'wb') as f: + f.write(content) + os.replace(tmp_cookie_path, COOKIES_PATH) config.set_runtime_override('cookiefile', COOKIES_PATH) log.info(f'Cookies file uploaded ({size} bytes)') return web.Response(text=serializer.encode({'status': 'ok', 'msg': f'Cookies uploaded ({size} bytes)'})) @@ -543,6 +561,22 @@ async def connect(sid, environ): await sio.emit('ytdl_options_changed', serializer.encode(get_options_update_time()), to=sid) def get_custom_dirs(): + cache_ttl_seconds = 5 + now = asyncio.get_running_loop().time() + cache_key = ( + config.DOWNLOAD_DIR, + config.AUDIO_DOWNLOAD_DIR, + config.CUSTOM_DIRS_EXCLUDE_REGEX, + ) + if ( + hasattr(get_custom_dirs, "_cache_key") + and hasattr(get_custom_dirs, "_cache_value") + and hasattr(get_custom_dirs, "_cache_time") + and get_custom_dirs._cache_key == cache_key + and (now - get_custom_dirs._cache_time) < cache_ttl_seconds + ): + return get_custom_dirs._cache_value + def recursive_dirs(base): path = pathlib.Path(base) @@ -579,10 +613,14 @@ def get_custom_dirs(): if config.DOWNLOAD_DIR != config.AUDIO_DOWNLOAD_DIR: audio_download_dir = recursive_dirs(config.AUDIO_DOWNLOAD_DIR) - return { + result = { "download_dir": download_dir, "audio_download_dir": audio_download_dir } + get_custom_dirs._cache_key = cache_key + get_custom_dirs._cache_time = now + get_custom_dirs._cache_value = result + return result @routes.get(config.URL_PREFIX) def index(request): diff --git a/app/tests/test_dl_formats.py b/app/tests/test_dl_formats.py new file mode 100644 index 0000000..5a1e92f --- /dev/null +++ b/app/tests/test_dl_formats.py @@ -0,0 +1,21 @@ +import unittest + +from app.dl_formats import get_format, get_opts + + +class DlFormatsTests(unittest.TestCase): + def test_audio_unknown_format_raises_value_error(self): + with self.assertRaises(ValueError): + get_format("audio", "auto", "invalid", "best") + + def test_wav_does_not_enable_thumbnail_postprocessing(self): + opts = get_opts("audio", "auto", "wav", "best", {}) + self.assertNotIn("writethumbnail", opts) + + def test_mp3_enables_thumbnail_postprocessing(self): + opts = get_opts("audio", "auto", "mp3", "best", {}) + self.assertTrue(opts.get("writethumbnail")) + + +if __name__ == "__main__": + unittest.main() diff --git a/app/ytdl.py b/app/ytdl.py index e738fe9..aa5df38 100644 --- a/app/ytdl.py +++ b/app/ytdl.py @@ -229,6 +229,12 @@ class DownloadInfo: class Download: manager = None + @classmethod + def shutdown_manager(cls): + if cls.manager is not None: + cls.manager.shutdown() + cls.manager = None + def __init__(self, download_dir, temp_dir, output_template, output_template_chapter, quality, format, ytdl_opts, info): self.download_dir = download_dir self.temp_dir = temp_dir @@ -568,20 +574,33 @@ class PersistentQueue: log_prefix = f"PersistentQueue:{self.identifier} repair (sqlite3/file)" log.debug(f"{log_prefix} started") try: - result = subprocess.run( - f"sqlite3 {self.path} '.recover' | sqlite3 {self.path}.tmp", + recover_proc = subprocess.Popen( + ["sqlite3", self.path, ".recover"], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + run_result = subprocess.run( + ["sqlite3", f"{self.path}.tmp"], + stdin=recover_proc.stdout, capture_output=True, text=True, - shell=True, - timeout=60 + timeout=60, ) - if result.stderr: - log.debug(f"{log_prefix} failed: {result.stderr}") + if recover_proc.stdout is not None: + recover_proc.stdout.close() + recover_stderr = recover_proc.stderr.read() if recover_proc.stderr is not None else "" + recover_proc.wait(timeout=60) + if run_result.stderr or recover_stderr: + error_text = " ".join(part for part in [recover_stderr.strip(), run_result.stderr.strip()] if part) + log.debug(f"{log_prefix} failed: {error_text}") else: shutil.move(f"{self.path}.tmp", self.path) - log.debug(f"{log_prefix}{result.stdout or " was successful, no output"}") + log.debug(f"{log_prefix}{run_result.stdout or ' was successful, no output'}") except FileNotFoundError: log.debug(f"{log_prefix} failed: 'sqlite3' was not found") + except subprocess.TimeoutExpired: + log.debug(f"{log_prefix} failed: sqlite recovery timed out") class DownloadQueue: def __init__(self, config, notifier): @@ -629,7 +648,7 @@ class DownloadQueue: if download.tmpfilename and os.path.isfile(download.tmpfilename): try: os.remove(download.tmpfilename) - except: + except OSError: pass download.info.status = 'error' download.close() @@ -898,7 +917,7 @@ class DownloadQueue: async def start_pending(self, ids): for id in ids: if not self.pending.exists(id): - log.warn(f'requested start for non-existent download {id}') + log.warning(f'requested start for non-existent download {id}') continue dl = self.pending.get(id) self.queue.put(dl) @@ -915,7 +934,7 @@ class DownloadQueue: await self.notifier.canceled(id) continue if not self.queue.exists(id): - log.warn(f'requested cancel for non-existent download {id}') + log.warning(f'requested cancel for non-existent download {id}') continue if self.queue.get(id).started(): self.queue.get(id).cancel() @@ -927,7 +946,7 @@ class DownloadQueue: async def clear(self, ids): for id in ids: if not self.done.exists(id): - log.warn(f'requested delete for non-existent download {id}') + log.warning(f'requested delete for non-existent download {id}') continue if self.config.DELETE_FILE_ON_TRASHCAN: dl = self.done.get(id) @@ -935,7 +954,7 @@ class DownloadQueue: dldirectory, _ = self.__calc_download_path(dl.info.download_type, dl.info.folder) os.remove(os.path.join(dldirectory, dl.info.filename)) except Exception as e: - log.warn(f'deleting file for download {id} failed with error message {e!r}') + log.warning(f'deleting file for download {id} failed with error message {e!r}') self.done.delete(id) await self.notifier.cleared(id) return {'status': 'ok'} diff --git a/ui/angular.json b/ui/angular.json index 6f0fddb..11245d1 100644 --- a/ui/angular.json +++ b/ui/angular.json @@ -33,9 +33,7 @@ "node_modules/@ng-select/ng-select/themes/default.theme.css", "src/styles.sass" ], - "scripts": [ - "node_modules/bootstrap/dist/js/bootstrap.bundle.min.js" - ], + "scripts": [], "serviceWorker": "ngsw-config.json", "browser": "src/main.ts", "polyfills": [ diff --git a/ui/src/app/app.config.ts b/ui/src/app/app.config.ts index b5fb6c0..6642eac 100644 --- a/ui/src/app/app.config.ts +++ b/ui/src/app/app.config.ts @@ -1,4 +1,4 @@ -import { ApplicationConfig, provideBrowserGlobalErrorListeners, isDevMode, provideZonelessChangeDetection, provideZoneChangeDetection } from '@angular/core'; +import { ApplicationConfig, provideBrowserGlobalErrorListeners, isDevMode, provideZoneChangeDetection } from '@angular/core'; import { provideServiceWorker } from '@angular/service-worker'; import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; diff --git a/ui/src/app/app.html b/ui/src/app/app.html index a54dddb..8ef50f8 100644 --- a/ui/src/app/app.html +++ b/ui/src/app/app.html @@ -49,22 +49,22 @@
-->