diff --git a/Dockerfile b/Dockerfile index bd9bd791..f9fee7d5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,11 +1,11 @@ # multi stage to build tube archivist # build python wheel, download and extract ffmpeg, copy into final image -FROM node:lts-alpine AS npm-builder +FROM node:22.12.0-alpine AS npm-builder COPY frontend/package.json frontend/package-lock.json / RUN npm i -FROM node:lts-alpine AS node-builder +FROM node:22.12.0-alpine AS node-builder # RUN npm config set registry https://registry.npmjs.org/ @@ -18,7 +18,7 @@ RUN npm run build:deploy WORKDIR / # First stage to build python wheel -FROM python:3.11.13-slim-bookworm AS builder +FROM python:3.13.11-slim-trixie AS builder RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential gcc libldap2-dev libsasl2-dev libssl-dev git @@ -28,7 +28,7 @@ COPY ./backend/requirements.txt /requirements.txt RUN pip install --user -r requirements.txt # build ffmpeg -FROM python:3.11.13-slim-bookworm AS ffmpeg-builder +FROM python:3.13.11-slim-trixie AS ffmpeg-builder ARG TARGETPLATFORM @@ -36,7 +36,7 @@ COPY docker_assets/ffmpeg_download.py ffmpeg_download.py RUN python ffmpeg_download.py $TARGETPLATFORM # build final image -FROM python:3.11.13-slim-bookworm AS tubearchivist +FROM python:3.13.11-slim-trixie AS tubearchivist ARG INSTALL_DEBUG diff --git a/backend/appsettings/index_mapping.json b/backend/appsettings/index_mapping.json index 8014aef8..79c257bf 100644 --- a/backend/appsettings/index_mapping.json +++ b/backend/appsettings/index_mapping.json @@ -12,6 +12,28 @@ "channel_id": { "type": "keyword" }, + "channel_active": { + "type": "boolean" + }, + "channel_banner_url": { + "type": "keyword", + "index": false + }, + "channel_thumb_url": { + "type": "keyword", + "index": false + }, + "channel_tvart_url": { + "type": "keyword", + "index": false + }, + "channel_description": { + "type": "text" + }, + "channel_last_refresh": { + "type": "date", + "format": "epoch_second" + }, "channel_name": { "type": "text", "analyzer": "english", @@ -28,38 +50,6 @@ } } }, - "channel_banner_url": { - "type": "keyword", - "index": false - }, - "channel_tvart_url": { - "type": "keyword", - "index": false - }, - "channel_thumb_url": { - "type": "keyword", - "index": false - }, - "channel_description": { - "type": "text" - }, - "channel_last_refresh": { - "type": "date", - "format": "epoch_second" - }, - "channel_tags": { - "type": "text", - "analyzer": "english", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "channel_tabs": { - "type": "keyword" - }, "channel_overwrites": { "properties": { "download_format": { @@ -84,6 +74,25 @@ "type": "long" } } + }, + "channel_subs": { + "type": "long" + }, + "channel_subscribed": { + "type": "boolean" + }, + "channel_tags": { + "type": "text", + "analyzer": "english", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "channel_tabs": { + "type": "keyword" } }, "expected_set": { @@ -101,23 +110,45 @@ { "index_name": "video", "expected_map": { - "vid_thumb_url": { - "type": "text", - "index": false + "active": { + "type": "boolean" }, - "vid_thumb_base64": { + "category": { "type": "text", - "index": false - }, - "date_downloaded": { - "type": "date", - "format": "epoch_second" + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } }, "channel": { "properties": { "channel_id": { "type": "keyword" }, + "channel_active": { + "type": "boolean" + }, + "channel_banner_url": { + "type": "keyword", + "index": false + }, + "channel_thumb_url": { + "type": "keyword", + "index": false + }, + "channel_tvart_url": { + "type": "keyword", + "index": false + }, + "channel_description": { + "type": "text" + }, + "channel_last_refresh": { + "type": "date", + "format": "epoch_second" + }, "channel_name": { "type": "text", "analyzer": "english", @@ -134,38 +165,6 @@ } } }, - "channel_banner_url": { - "type": "keyword", - "index": false - }, - "channel_tvart_url": { - "type": "keyword", - "index": false - }, - "channel_thumb_url": { - "type": "keyword", - "index": false - }, - "channel_description": { - "type": "text" - }, - "channel_last_refresh": { - "type": "date", - "format": "epoch_second" - }, - "channel_tags": { - "type": "text", - "analyzer": "english", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "channel_tabs": { - "type": "keyword" - }, "channel_overwrites": { "properties": { "download_format": { @@ -190,87 +189,44 @@ "type": "long" } } - } - } - }, - "description": { - "type": "text" - }, - "media_url": { - "type": "keyword", - "index": false - }, - "media_size": { - "type": "long" - }, - "tags": { - "type": "text", - "analyzer": "english", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256 - } - } - }, - "title": { - "type": "text", - "analyzer": "english", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256, - "normalizer": "to_lower" }, - "search_as_you_type": { - "type": "search_as_you_type", - "doc_values": false, - "max_shingle_size": 3 - } - } - }, - "vid_last_refresh": { - "type": "date", - "format": "epoch_second" - }, - "youtube_id": { - "type": "keyword" - }, - "vid_type": { - "type": "keyword" - }, - "published": { - "type": "date", - "format": "epoch_second||strict_date_optional_time" - }, - "playlist": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256, - "normalizer": "to_lower" + "channel_subs": { + "type": "long" + }, + "channel_subscribed": { + "type": "boolean" + }, + "channel_tags": { + "type": "text", + "analyzer": "english", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "channel_tabs": { + "type": "keyword" } } }, "comment_count": { "type": "long" }, - "stats": { - "properties": { - "average_rating": { - "type": "float" - }, - "dislike_count": { - "type": "long" - }, - "like_count": { - "type": "long" - }, - "view_count": { - "type": "long" - } - } + "date_downloaded": { + "type": "date", + "format": "epoch_second" + }, + "description": { + "type": "text" + }, + "media_size": { + "type": "long" + }, + "media_url": { + "type": "keyword", + "index": false }, "player": { "properties": { @@ -290,68 +246,32 @@ } } }, - "subtitles": { - "properties": { - "ext": { + "playlist": { + "type": "text", + "fields": { + "keyword": { "type": "keyword", - "index": false - }, - "lang": { - "type": "keyword", - "index": false - }, - "media_url": { - "type": "keyword", - "index": false - }, - "name": { - "type": "keyword" - }, - "source": { - "type": "keyword" - }, - "url": { - "type": "keyword", - "index": false + "ignore_above": 256, + "normalizer": "to_lower" } } }, - "streams": { - "properties": { - "type": { - "type": "keyword", - "index": false - }, - "index": { - "type": "short", - "index": false - }, - "codec": { - "type": "text" - }, - "width": { - "type": "short" - }, - "height": { - "type": "short" - }, - "bitrate": { - "type": "integer" - } - } + "published": { + "type": "date", + "format": "epoch_second||strict_date_optional_time" }, "sponsorblock": { "properties": { - "last_refresh": { - "type": "date", - "format": "epoch_second" - }, "has_unlocked": { "type": "boolean" }, "is_enabled": { "type": "boolean" }, + "last_refresh": { + "type": "date", + "format": "epoch_second" + }, "segments": { "properties": { "UUID": { @@ -387,6 +307,112 @@ } } } + }, + "stats": { + "properties": { + "average_rating": { + "type": "float" + }, + "dislike_count": { + "type": "long" + }, + "like_count": { + "type": "long" + }, + "view_count": { + "type": "long" + } + } + }, + "streams": { + "properties": { + "bitrate": { + "type": "integer" + }, + "codec": { + "type": "text" + }, + "height": { + "type": "short" + }, + "index": { + "type": "short", + "index": false + }, + "type": { + "type": "keyword", + "index": false + }, + "width": { + "type": "short" + } + } + }, + "subtitles": { + "properties": { + "ext": { + "type": "keyword", + "index": false + }, + "lang": { + "type": "keyword", + "index": false + }, + "media_url": { + "type": "keyword", + "index": false + }, + "name": { + "type": "keyword" + }, + "source": { + "type": "keyword" + }, + "url": { + "type": "keyword", + "index": false + } + } + }, + "tags": { + "type": "text", + "analyzer": "english", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256 + } + } + }, + "title": { + "type": "text", + "analyzer": "english", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256, + "normalizer": "to_lower" + }, + "search_as_you_type": { + "type": "search_as_you_type", + "doc_values": false, + "max_shingle_size": 3 + } + } + }, + "vid_last_refresh": { + "type": "date", + "format": "epoch_second" + }, + "vid_thumb_url": { + "type": "text", + "index": false + }, + "vid_type": { + "type": "keyword" + }, + "youtube_id": { + "type": "keyword" } }, "expected_set": { @@ -404,13 +430,15 @@ { "index_name": "download", "expected_map": { - "timestamp": { - "type": "date", - "format": "epoch_second" + "auto_start": { + "type": "boolean" }, "channel_id": { "type": "keyword" }, + "channel_indexed": { + "type": "boolean" + }, "channel_name": { "type": "text", "fields": { @@ -421,9 +449,23 @@ } } }, + "duration": { + "type": "keyword" + }, + "message": { + "type": "text" + }, + "published": { + "type": "date", + "format": "epoch_second||strict_date_optional_time" + }, "status": { "type": "keyword" }, + "timestamp": { + "type": "date", + "format": "epoch_second" + }, "title": { "type": "text", "fields": { @@ -434,24 +476,14 @@ } } }, - "published": { - "type": "date", - "format": "epoch_second||strict_date_optional_time" - }, "vid_thumb_url": { "type": "keyword" }, - "youtube_id": { - "type": "keyword" - }, "vid_type": { "type": "keyword" }, - "auto_start": { - "type": "boolean" - }, - "message": { - "type": "text" + "youtube_id": { + "type": "keyword" } }, "expected_set": { @@ -469,37 +501,9 @@ { "index_name": "playlist", "expected_map": { - "playlist_id": { - "type": "keyword" - }, - "playlist_description": { - "type": "text" - }, - "playlist_subscribed": { - "type": "boolean" - }, - "playlist_type": { - "type": "keyword" - }, "playlist_active": { "type": "boolean" }, - "playlist_name": { - "type": "text", - "analyzer": "english", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256, - "normalizer": "to_lower" - }, - "search_as_you_type": { - "type": "search_as_you_type", - "doc_values": false, - "max_shingle_size": 3 - } - } - }, "playlist_channel": { "type": "text", "fields": { @@ -513,15 +517,8 @@ "playlist_channel_id": { "type": "keyword" }, - "playlist_thumbnail": { - "type": "keyword" - }, - "playlist_last_refresh": { - "type": "date", - "format": "epoch_second" - }, - "playlist_sort_order": { - "type": "keyword" + "playlist_description": { + "type": "text" }, "playlist_entries": { "properties": { @@ -557,6 +554,41 @@ "type": "keyword" } } + }, + "playlist_id": { + "type": "keyword" + }, + "playlist_last_refresh": { + "type": "date", + "format": "epoch_second" + }, + "playlist_name": { + "type": "text", + "analyzer": "english", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256, + "normalizer": "to_lower" + }, + "search_as_you_type": { + "type": "search_as_you_type", + "doc_values": false, + "max_shingle_size": 3 + } + } + }, + "playlist_sort_order": { + "type": "keyword" + }, + "playlist_subscribed": { + "type": "boolean" + }, + "playlist_thumbnail": { + "type": "keyword" + }, + "playlist_type": { + "type": "keyword" } }, "expected_set": { @@ -574,22 +606,6 @@ { "index_name": "subtitle", "expected_map": { - "youtube_id": { - "type": "keyword" - }, - "title": { - "type": "text", - "fields": { - "keyword": { - "type": "keyword", - "ignore_above": 256, - "normalizer": "to_lower" - } - } - }, - "subtitle_fragment_id": { - "type": "keyword" - }, "subtitle_channel": { "type": "text", "fields": { @@ -603,15 +619,11 @@ "subtitle_channel_id": { "type": "keyword" }, - "subtitle_start": { - "type": "text" - }, "subtitle_end": { "type": "text" }, - "subtitle_last_refresh": { - "type": "date", - "format": "epoch_second" + "subtitle_fragment_id": { + "type": "keyword" }, "subtitle_index": { "type": "long" @@ -619,12 +631,32 @@ "subtitle_lang": { "type": "keyword" }, - "subtitle_source": { - "type": "keyword" + "subtitle_last_refresh": { + "type": "date", + "format": "epoch_second" }, "subtitle_line": { "type": "text", "analyzer": "english" + }, + "subtitle_source": { + "type": "keyword" + }, + "subtitle_start": { + "type": "text" + }, + "title": { + "type": "text", + "fields": { + "keyword": { + "type": "keyword", + "ignore_above": 256, + "normalizer": "to_lower" + } + } + }, + "youtube_id": { + "type": "keyword" } }, "expected_set": { @@ -642,37 +674,11 @@ { "index_name": "comment", "expected_map": { - "youtube_id": { - "type": "keyword" - }, - "comment_last_refresh": { - "type": "date", - "format": "epoch_second" - }, "comment_channel_id": { "type": "keyword" }, "comment_comments": { "properties": { - "comment_id": { - "type": "keyword" - }, - "comment_text": { - "type": "text" - }, - "comment_timestamp": { - "type": "date", - "format": "epoch_second" - }, - "comment_time_text": { - "type": "text" - }, - "comment_likecount": { - "type": "long" - }, - "comment_is_favorited": { - "type": "boolean" - }, "comment_author": { "type": "text", "fields": { @@ -686,16 +692,42 @@ "comment_author_id": { "type": "keyword" }, - "comment_author_thumbnail": { - "type": "keyword" - }, "comment_author_is_uploader": { "type": "boolean" }, + "comment_author_thumbnail": { + "type": "keyword" + }, + "comment_id": { + "type": "keyword" + }, + "comment_is_favorited": { + "type": "boolean" + }, + "comment_likecount": { + "type": "long" + }, "comment_parent": { "type": "keyword" + }, + "comment_text": { + "type": "text" + }, + "comment_time_text": { + "type": "text" + }, + "comment_timestamp": { + "type": "date", + "format": "epoch_second" } } + }, + "comment_last_refresh": { + "type": "date", + "format": "epoch_second" + }, + "youtube_id": { + "type": "keyword" } }, "expected_set": { diff --git a/backend/appsettings/serializers.py b/backend/appsettings/serializers.py index 734b3131..3f145696 100644 --- a/backend/appsettings/serializers.py +++ b/backend/appsettings/serializers.py @@ -44,7 +44,6 @@ class AppConfigDownloadsSerializer( format = serializers.CharField(allow_null=True) format_sort = serializers.CharField(allow_null=True) add_metadata = serializers.BooleanField() - add_thumbnail = serializers.BooleanField() subtitle = serializers.CharField(allow_null=True) subtitle_source = serializers.ChoiceField( choices=["auto", "user"], allow_null=True @@ -55,6 +54,7 @@ class AppConfigDownloadsSerializer( choices=["top", "new"], allow_null=True ) cookie_import = serializers.BooleanField() + pot_provider_url = serializers.CharField(allow_null=True) potoken = serializers.BooleanField() throttledratelimit = serializers.IntegerField(allow_null=True) extractor_lang = serializers.CharField(allow_null=True) diff --git a/backend/appsettings/src/backup.py b/backend/appsettings/src/backup.py index d977e521..702acd99 100644 --- a/backend/appsettings/src/backup.py +++ b/backend/appsettings/src/backup.py @@ -7,6 +7,7 @@ Functionality: import json import os +import re import zipfile from datetime import datetime @@ -19,7 +20,10 @@ from task.models import CustomPeriodicTask class ElasticBackup: """dump index to nd-json files for later bulk import""" - INDEX_SPLIT = ["comment"] + INDEX_SIZE_CONF = { + "comment": 200, + "subtitle": 10000, + } CACHE_DIR = EnvironmentSettings.CACHE_DIR BACKUP_DIR = os.path.join(CACHE_DIR, "backup") @@ -62,8 +66,8 @@ class ElasticBackup: "total": self._get_total(index_name), } - if index_name in self.INDEX_SPLIT: - paginate_kwargs.update({"size": 200}) + if size_overwrite := self.INDEX_SIZE_CONF.get(index_name): + paginate_kwargs.update({"size": size_overwrite}) paginate = IndexPaginate(f"ta_{index_name}", **paginate_kwargs) _ = paginate.get_results() @@ -98,8 +102,7 @@ class ElasticBackup: def post_bulk_restore(self, file_name): """send bulk to es""" - file_path = os.path.join(self.CACHE_DIR, file_name) - with open(file_path, "r", encoding="utf-8") as f: + with open(file_name, "r", encoding="utf-8") as f: data = f.read() if not data.strip(): @@ -156,6 +159,7 @@ class ElasticBackup: call reset from ElasticIndexWrap first to start blank """ zip_content = self._unpack_zip_backup(filename) + zip_content.sort() self._restore_json_files(zip_content) def _unpack_zip_backup(self, filename): @@ -252,7 +256,7 @@ class BackupCallback: for document in self.source: document_id = document["_id"] - es_index = document["_index"] + es_index = re.sub(r"_v\d+$", "", document["_index"]) # remove _v action = {"index": {"_index": es_index, "_id": document_id}} source = document["_source"] bulk_list.append(json.dumps(action)) diff --git a/backend/appsettings/src/config.py b/backend/appsettings/src/config.py index 2fb9247a..f557b459 100644 --- a/backend/appsettings/src/config.py +++ b/backend/appsettings/src/config.py @@ -35,13 +35,13 @@ class DownloadsConfigType(TypedDict): format: str | None format_sort: str | None add_metadata: bool - add_thumbnail: bool subtitle: str | None subtitle_source: Literal["user", "auto"] | None subtitle_index: bool comment_max: str | None comment_sort: Literal["top", "new"] | None cookie_import: bool + pot_provider_url: str | None potoken: bool throttledratelimit: int | None extractor_lang: str | None @@ -85,13 +85,13 @@ class AppConfig: "format": None, "format_sort": None, "add_metadata": False, - "add_thumbnail": False, "subtitle": None, "subtitle_source": None, "subtitle_index": False, "comment_max": None, "comment_sort": "top", "cookie_import": False, + "pot_provider_url": None, "potoken": False, "throttledratelimit": None, "extractor_lang": None, diff --git a/backend/appsettings/src/index_setup.py b/backend/appsettings/src/index_setup.py index ae1a1438..e39ab9e2 100644 --- a/backend/appsettings/src/index_setup.py +++ b/backend/appsettings/src/index_setup.py @@ -5,83 +5,109 @@ functionality: - backup and restore metadata """ +from enum import Enum, auto + from appsettings.src.backup import ElasticBackup from appsettings.src.config import AppConfig from appsettings.src.snapshot import ElasticSnapshot from common.src.es_connect import ElasticWrap from common.src.helper import get_mapping +from deepdiff import DeepDiff +from django.conf import settings + + +class MappingAction(Enum): + """index action options""" + + NOOP = auto() + PUT_MAPPING = auto() + REINDEX = auto() class ElasticIndex: """interact with a single index""" + REINDEX_KEYS = { + "type", + "analyzer", + "search_analyzer", + "normalizer", + "index", + "doc_values", + "norms", + "ignore_above", + "enabled", + "format", + } + def __init__(self, index_name, expected_map=False, expected_set=False): self.index_name = index_name self.expected_map = expected_map self.expected_set = expected_set self.exists, self.details = self.index_exists() + @property + def index_namespace(self) -> str: + """namespaced index""" + return f"ta_{self.index_name}" + def index_exists(self): """check if index already exists and return mapping if it does""" - response, status_code = ElasticWrap(f"ta_{self.index_name}").get() + response, status_code = ElasticWrap(self.index_namespace).get() exists = status_code == 200 - details = response.get(f"ta_{self.index_name}", False) + if not exists: + return False, False + + index_key = f"{self.index_namespace}" + current_version = self.get_current_version() + if current_version: + index_key += f"_v{current_version}" + + details = response.get(index_key, False) return exists, details - def validate(self): + def get_current_version(self) -> None | int: + """get current version from aliases of index""" + response, _ = ElasticWrap(f"{self.index_namespace}/_alias").get() + if not response: + raise ValueError("failed to fetch aliases: ", response) + + alias_name = list(response.keys()) + if not alias_name: + return None + + version_str = alias_name[0].lstrip(f"{self.index_namespace}_v") + if not version_str: + # is initial version + return None + + if not version_str.isdigit(): + raise ValueError("unexpected version_str: ", version_str) + + return int(version_str) + + def validate(self) -> tuple[MappingAction, set[str]]: """ check if all expected mappings and settings match returns True when rebuild is needed """ - - if self.expected_map or self.expected_map == {}: - rebuild = self.validate_mappings() - if rebuild: - return rebuild + mapping_diff = self._get_mapping_diff() + removed_fields = self._get_fields_to_delete(diff=mapping_diff) if self.expected_set: - rebuild = self.validate_settings() - if rebuild: - return rebuild + settings_diff = self._validate_settings() + if settings_diff: + # treat settings diff as full reindex + return MappingAction.REINDEX, removed_fields - return False + if self.expected_map or self.expected_map == {}: + action = self._classify_mapping_diff(diff=mapping_diff) + return action, removed_fields - def validate_mappings(self): - """check if all mappings are as expected""" - now_map = self.details["mappings"].get("properties", {}) + return MappingAction.NOOP, removed_fields - for key, value in self.expected_map.items(): - # nested - if list(value.keys()) == ["properties"]: - for key_n, value_n in value["properties"].items(): - if key not in now_map: - print(f"detected mapping change: {key_n}, {value_n}") - return True - if key_n not in now_map[key]["properties"].keys(): - print(f"detected mapping change: {key_n}, {value_n}") - return True - if not value_n == now_map[key]["properties"][key_n]: - print(f"detected mapping change: {key_n}, {value_n}") - return True - - continue - - # not nested - if key not in now_map.keys(): - print(f"detected mapping change: {key}, {value}") - return True - if not value == now_map[key]: - print(f"detected mapping change: {key}, {value}") - return True - - # simple doc store - if self.expected_map == {} and now_map != self.expected_map: - return True - - return False - - def validate_settings(self): + def _validate_settings(self): """check if all settings are as expected""" now_set = self.details["settings"]["index"] @@ -97,44 +123,94 @@ class ElasticIndex: return False - def rebuild_index(self): + def _get_mapping_diff(self) -> DeepDiff: + """check if all mappings are as expected""" + now_map = self.details.get("mappings", {}).get("properties", {}) + diff = DeepDiff( + now_map, + self.expected_map, + ignore_order=True, + report_repetition=True, + view="tree", + ) + if diff: + print(f"[{self.index_namespace}] detected mapping change") + if settings.DEBUG: + print(f"[{self.index_namespace}] mapping change: {diff}") + + return diff + + def _classify_mapping_diff(self, diff: DeepDiff) -> MappingAction: + """use diff to detect what to do""" + if not diff: + return MappingAction.NOOP + + if diff.get("type_changes"): + # always incompatible, needs reindex + return MappingAction.REINDEX + + added = diff.get("dictionary_item_added", []) + has_additions = bool(added) + + for item in diff.get("values_changed", []): + path = item.path(output_format="list") + if not path: + continue + + if path[-1] in self.REINDEX_KEYS: + return MappingAction.REINDEX + + # compatible addition + has_additions = True + + if has_additions: + return MappingAction.PUT_MAPPING + + return MappingAction.NOOP + + def _get_fields_to_delete(self, diff: DeepDiff) -> set[str]: + """fields to remove during next reindex""" + removed_fields = set() + for item in diff.get("dictionary_item_removed", []): + value = item.t1 or {} + is_field_definition = "type" in value or "properties" in value + if not is_field_definition: + continue + + path = item.path(output_format="list") + removed_fields.add(".".join(path)) + + return removed_fields + + def rebuild_index(self, removed_fields: set[str]): """rebuild with new mapping""" - print(f"applying new mappings to index ta_{self.index_name}...") - self.create_blank(for_backup=True) - self.reindex("backup") - self.delete_index(backup=False) - self.create_blank() - self.reindex("restore") - self.delete_index() + print(f"[{self.index_namespace}] applying new mappings to index") + current_version = self.get_current_version() - def reindex(self, method): - """create on elastic search""" - if method == "backup": - source = f"ta_{self.index_name}" - destination = f"ta_{self.index_name}_backup" - elif method == "restore": - source = f"ta_{self.index_name}_backup" - destination = f"ta_{self.index_name}" - else: - raise ValueError("invalid method, expected 'backup' or 'restore'") + new_version = current_version + 1 if current_version else 2 - data = {"source": {"index": source}, "dest": {"index": destination}} - _, _ = ElasticWrap("_reindex?refresh=true").post(data=data) + self.create_blank(new_version=new_version) + self.reindex(new_version=new_version, removed_fields=removed_fields) + self.delete_index(by_version=current_version) + self.create_alias(new_version=new_version, old_version=current_version) - def delete_index(self, backup=True): + def delete_index(self, by_version: int | None): """delete index passed as argument""" - path = f"ta_{self.index_name}" - if backup: - path = path + "_backup" + path = self.index_namespace + if by_version is not None: + path += f"_v{by_version}" - _, _ = ElasticWrap(path).delete() + print(f"[{path}] delete index") + response, status_code = ElasticWrap(path).delete() + if status_code not in [200, 201]: + print(f"{status_code}: {response}") + raise ValueError("index delete failed") - def create_blank(self, for_backup=False): - """apply new mapping and settings for blank new index""" - print(f"create new blank index with name ta_{self.index_name}...") - path = f"ta_{self.index_name}" - if for_backup: - path = f"{path}_backup" + def create_blank(self, new_version: int | None = None): + """create blank""" + path = self.index_namespace + if new_version is not None: + path += f"_v{new_version}" data = {} if self.expected_set: @@ -145,7 +221,98 @@ class ElasticIndex: # no indexing for config data["mappings"]["dynamic"] = False - _, _ = ElasticWrap(path).put(data) + print(f"[{path}] create new blank index") + if settings.DEBUG: + print(f"[{path}] creat new blank index with data: {data}") + + response, status_code = ElasticWrap(path).put(data) + if status_code not in [200, 201]: + print(f"{status_code}: {response}") + raise ValueError(f"create blank index {path} failed") + + def reindex(self, new_version: int, removed_fields: set[str]): + """reindex to versioned new index after creating""" + source = self.index_namespace + dest = f"{self.index_namespace}_v{new_version}" + data: dict = {"source": {"index": source}, "dest": {"index": dest}} + + if removed_fields: + script = "\n".join( + f"ctx._source.remove('{i}');" for i in removed_fields + ) + data["script"] = {"lang": "painless", "source": script} + + msg = f"[{self.index_namespace}] reindex from {source} to {dest}" + if removed_fields: + msg += f", remove unexpected fields: {removed_fields}" + + print(msg) + + if settings.DEBUG: + print(f"send data: {data}") + + path = "_reindex?refresh=true" + response, status_code = ElasticWrap(path).post(data=data) + if status_code not in [200, 201]: + print(f"{status_code}: {response}") + raise ValueError("reindex failed failed") + + def create_alias(self, new_version: int, old_version: int | None = None): + """create aliast for moved index""" + index_new = f"{self.index_namespace}_v{new_version}" + index_old = None + + data: dict = { + "actions": [ + { + "add": { + "index": index_new, + "alias": self.index_namespace, + "is_write_index": True, + }, + }, + ] + } + if old_version: + index_old = f"{self.index_namespace}_v{old_version}" + data["actions"].append( + { + "remove": { + "index": index_old, + "alias": self.index_namespace, + } + } + ) + + message = f"create new alias {index_new}" + if index_old: + message += f", remove old alias {index_old}" + + print(f"[{self.index_namespace}] {message}") + if settings.DEBUG: + print(f"create alias with data: {data}") + + response, status_code = ElasticWrap("_alias").put(data=data) + if status_code not in [200, 201]: + print(f"{status_code}: {response}") + raise ValueError("alias update failed") + + def mapping_update(self): + """simple mapping update only, use migrations for defaults""" + current_version = self.get_current_version() + path = self.index_namespace + if current_version is not None: + path += f"_v{current_version}" + + data = {"properties": self.expected_map} + print(f"[{path}] update mapping") + if settings.DEBUG: + print(f"[{path}] update mapping with data: {data}") + + response, status_code = ElasticWrap(f"{path}/_mapping").put(data) + if status_code not in [200, 201]: + print(f"{status_code}: {response}") + raise ValueError(f"create blank index {path} failed") class ElasticIndexWrap: @@ -164,14 +331,22 @@ class ElasticIndexWrap: handler.create_blank() continue - rebuild = handler.validate() - if rebuild: + action, removed_fields = handler.validate() + if action == MappingAction.REINDEX: self._check_backup() - handler.rebuild_index() + handler.rebuild_index(removed_fields) continue - # else all good - print(f"ta_{index_name} index is created and up to date...") + if action == MappingAction.PUT_MAPPING: + handler.mapping_update() + + if removed_fields: + print( + f"[ta_{index_name}] skip removing unexpected fields:" + + f" {removed_fields}" + ) + else: + print(f"[ta_{index_name}] index status is as expected.") def reset(self): """reset all indexes to blank""" @@ -180,11 +355,15 @@ class ElasticIndexWrap: def delete_all(self): """delete all indexes""" - print("reset elastic index") for index in self.index_config: index_name, _, _ = self._config_split(index) + print(f"[ta_{index_name}] reset elastic index") handler = ElasticIndex(index_name) - handler.delete_index(backup=False) + if not handler.exists: + continue + + current_version = handler.get_current_version() + handler.delete_index(by_version=current_version) def create_all_blank(self): """create all blank indexes""" diff --git a/backend/appsettings/src/snapshot.py b/backend/appsettings/src/snapshot.py index 49c7eccd..a0739854 100644 --- a/backend/appsettings/src/snapshot.py +++ b/backend/appsettings/src/snapshot.py @@ -261,10 +261,15 @@ class ElasticSnapshot: def restore_all(self, snapshot_name): """restore snapshot by name""" for index in self.all_indices: - _, _ = ElasticWrap(index).delete() + response, status_code = ElasticWrap(index).get() + if status_code == 404: + continue + + index_alias = list(response.keys())[0] + _, _ = ElasticWrap(index_alias).delete() path = f"_snapshot/{self.REPO}/{snapshot_name}/_restore" - data = {"indices": "*"} + data = {"indices": "*,-.*"} response, statuscode = ElasticWrap(path).post(data=data) if statuscode == 200: print(f"snapshot: executing now: {response}") diff --git a/backend/channel/serializers.py b/backend/channel/serializers.py index 1f364a97..d2dc4184 100644 --- a/backend/channel/serializers.py +++ b/backend/channel/serializers.py @@ -34,10 +34,12 @@ class ChannelSerializer(serializers.Serializer): channel_id = serializers.CharField() channel_active = serializers.BooleanField() - channel_banner_url = serializers.CharField() - channel_thumb_url = serializers.CharField() - channel_tvart_url = serializers.CharField() - channel_description = serializers.CharField() + channel_banner_url = serializers.CharField(allow_null=True, required=False) + channel_thumb_url = serializers.CharField(allow_null=True, required=False) + channel_tvart_url = serializers.CharField(allow_null=True, required=False) + channel_description = serializers.CharField( + allow_null=True, required=False + ) channel_last_refresh = serializers.CharField() channel_name = serializers.CharField() channel_overwrites = ChannelOverwriteSerializer(required=False) @@ -49,7 +51,6 @@ class ChannelSerializer(serializers.Serializer): channel_tabs = serializers.ListField( child=serializers.ChoiceField(VideoTypeEnum.values_known()) ) - channel_views = serializers.IntegerField() _index = serializers.CharField(required=False) _score = serializers.IntegerField(required=False) diff --git a/backend/channel/src/index.py b/backend/channel/src/index.py index 37f2e625..390f9023 100644 --- a/backend/channel/src/index.py +++ b/backend/channel/src/index.py @@ -57,54 +57,56 @@ class YoutubeChannel(YouTubeItem): """extract relevant fields""" self.youtube_meta["thumbnails"].reverse() channel_name = self.youtube_meta["uploader"] or self.youtube_meta["id"] + description = self.youtube_meta.get("description") or None self.json_data = { "channel_active": True, - "channel_description": self.youtube_meta.get("description", ""), + "channel_description": description, "channel_id": self.youtube_id, "channel_last_refresh": int(datetime.now().timestamp()), "channel_name": channel_name, - "channel_subs": self.youtube_meta.get("channel_follower_count", 0), + "channel_subs": self.youtube_meta.get("channel_follower_count") + or 0, "channel_subscribed": False, "channel_tags": self.youtube_meta.get("tags", []), - "channel_banner_url": self._get_banner_art(), - "channel_thumb_url": self._get_thumb_art(), - "channel_tvart_url": self._get_tv_art(), - "channel_views": self.youtube_meta.get("view_count") or 0, "channel_tabs": self.get_channel_tabs(), } - def _get_thumb_art(self): + self._get_thumb_art() + self._get_tv_art() + self._get_banner_art() + + def _get_thumb_art(self) -> None: """extract thumb art""" for i in self.youtube_meta["thumbnails"]: if not i.get("width"): continue if i.get("width") == i.get("height"): - return i["url"] + self.json_data["channel_thumb_url"] = i["url"] + return - return False - - def _get_tv_art(self): + def _get_tv_art(self) -> None: """extract tv artwork""" for i in self.youtube_meta["thumbnails"]: if i.get("id") == "banner_uncropped": - return i["url"] + self.json_data["channel_tvart_url"] = i["url"] + return for i in self.youtube_meta["thumbnails"]: if not i.get("width"): continue if i["width"] // i["height"] < 2 and not i["width"] == i["height"]: - return i["url"] + self.json_data["channel_tvart_url"] = i["url"] + return - return False + return - def _get_banner_art(self): + def _get_banner_art(self) -> None: """extract banner artwork""" for i in self.youtube_meta["thumbnails"]: if not i.get("width"): continue if i["width"] // i["height"] > 5: - return i["url"] - - return False + self.json_data["channel_banner_url"] = i["url"] + return def get_channel_tabs(self) -> list[str]: """get channel tabs""" @@ -132,51 +134,39 @@ class YoutubeChannel(YouTubeItem): self.json_data = { "channel_active": False, "channel_last_refresh": int(datetime.now().timestamp()), - "channel_subs": fallback.get("channel_follower_count", 0), + "channel_subs": fallback.get("channel_follower_count") or 0, "channel_name": fallback["uploader"], - "channel_banner_url": False, - "channel_tvart_url": False, "channel_id": self.youtube_id, "channel_subscribed": False, "channel_tags": [], - "channel_description": "", - "channel_thumb_url": False, - "channel_views": 0, } def get_channel_art(self): """download channel art for new channels""" urls = ( - self.json_data["channel_thumb_url"], - self.json_data["channel_banner_url"], - self.json_data["channel_tvart_url"], + self.json_data.get("channel_thumb_url"), + self.json_data.get("channel_banner_url"), + self.json_data.get("channel_tvart_url"), ) ThumbManager(self.youtube_id, item_type="channel").download(urls) def sync_to_videos(self): """sync new channel_dict to all videos of channel""" - # add ingest pipeline - processors = [] - for field, value in self.json_data.items(): - if value is None: - line = { - "script": { - "lang": "painless", - "source": f"ctx['{field}'] = null;", - } - } - else: - line = {"set": {"field": "channel." + field, "value": value}} - - processors.append(line) - - data = {"description": self.youtube_id, "processors": processors} - ingest_path = f"_ingest/pipeline/{self.youtube_id}" - _, _ = ElasticWrap(ingest_path).put(data) - # apply pipeline - data = {"query": {"match": {"channel.channel_id": self.youtube_id}}} - update_path = f"ta_video/_update_by_query?pipeline={self.youtube_id}" - _, _ = ElasticWrap(update_path).post(data) + data = { + "query": { + "term": {"channel.channel_id": {"value": self.youtube_id}}, + }, + "script": { + "lang": "painless", + "params": {"channel": self.json_data}, + "source": "ctx._source.channel = params.channel", + }, + } + update_path = "ta_video/_update_by_query" + response, status_code = ElasticWrap(update_path).post(data) + if status_code not in [200, 201]: + print(f"sync to videos failed with status code {status_code}") + print(response) def change_subscribe(self, new_subscribe_state: bool): """change subscribe status""" diff --git a/backend/common/src/es_connect.py b/backend/common/src/es_connect.py index b2d11635..ecd46661 100644 --- a/backend/common/src/es_connect.py +++ b/backend/common/src/es_connect.py @@ -168,7 +168,7 @@ class IndexPaginate: def get_pit(self): """get pit for index""" - path = f"{self.index_name}/_pit?keep_alive=10m" + path = f"{self.index_name}/_pit?keep_alive=15m" response, _ = ElasticWrap(path).post() self.pit_id = response["id"] @@ -184,7 +184,7 @@ class IndexPaginate: self.data.update({"sort": [{"_doc": {"order": "desc"}}]}) self.data["size"] = self.kwargs.get("size") or self.DEFAULT_SIZE - self.data["pit"] = {"id": self.pit_id, "keep_alive": "10m"} + self.data["pit"] = {"id": self.pit_id, "keep_alive": "15m"} def run_loop(self): """loop through results until last hit""" diff --git a/backend/common/src/search_processor.py b/backend/common/src/search_processor.py index e0ceaefc..f952547c 100644 --- a/backend/common/src/search_processor.py +++ b/backend/common/src/search_processor.py @@ -56,17 +56,17 @@ class SearchProcess: """detect which type of data to process""" index = result["_index"] processed = False - if index == "ta_video": + if index.startswith("ta_video"): processed = self._process_video(result["_source"]) - if index == "ta_channel": + if index.startswith("ta_channel"): processed = self._process_channel(result["_source"]) - if index == "ta_playlist": + if index.startswith("ta_playlist"): processed = self._process_playlist(result["_source"]) - if index == "ta_download": + if index.startswith("ta_download"): processed = self._process_download(result["_source"]) - if index == "ta_comment": + if index.startswith("ta_comment"): processed = self._process_comment(result["_source"]) - if index == "ta_subtitle": + if index.startswith("ta_subtitle"): processed = self._process_subtitle(result) if isinstance(processed, dict): @@ -89,12 +89,25 @@ class SearchProcess: channel_dict.update( { "channel_last_refresh": date_str, - "channel_banner_url": f"{art_base}_banner.jpg", - "channel_thumb_url": f"{art_base}_thumb.jpg", - "channel_tvart_url": f"{art_base}_tvart.jpg", + "channel_description": channel_dict.get("channel_description"), } ) + if channel_dict.get("channel_banner_url"): + channel_dict["channel_banner_url"] = f"{art_base}_banner.jpg" + else: + channel_dict["channel_banner_url"] = None + + if channel_dict.get("channel_thumb_url"): + channel_dict["channel_thumb_url"] = f"{art_base}_thumb.jpg" + else: + channel_dict["channel_thumb_url"] = None + + if channel_dict.get("channel_tvart_url"): + channel_dict["channel_tvart_url"] = f"{art_base}_tvart.jpg" + else: + channel_dict["channel_tvart_url"] = None + return dict(sorted(channel_dict.items())) def _process_video(self, video_dict): @@ -125,6 +138,7 @@ class SearchProcess: "vid_last_refresh": vid_last_refresh, "published": published, "vid_thumb_url": f"{cache_root}/{vid_thumb_url}", + "description": video_dict.get("description"), } ) @@ -154,10 +168,12 @@ class SearchProcess: ) cache_root = EnvironmentSettings().get_cache_root() playlist_thumbnail = f"{cache_root}/playlists/{playlist_id}.jpg" + description = playlist_dict.get("playlist_description") playlist_dict.update( { "playlist_thumbnail": playlist_thumbnail, "playlist_last_refresh": playlist_last_refresh, + "playlist_description": description, } ) diff --git a/backend/config/management/commands/ta_startup.py b/backend/config/management/commands/ta_startup.py index 7c7ae349..dc5e8aa2 100644 --- a/backend/config/management/commands/ta_startup.py +++ b/backend/config/management/commands/ta_startup.py @@ -12,9 +12,10 @@ from time import sleep from appsettings.src.config import AppConfig, ReleaseVersion from appsettings.src.index_setup import ElasticIndexWrap from appsettings.src.snapshot import ElasticSnapshot +from channel.src.index import YoutubeChannel from common.src.env_settings import EnvironmentSettings -from common.src.es_connect import ElasticWrap -from common.src.helper import clear_dl_cache +from common.src.es_connect import ElasticWrap, IndexPaginate +from common.src.helper import clear_dl_cache, get_channels from common.src.ta_redis import RedisArchivist from django.core.management.base import BaseCommand, CommandError from django.utils import dateformat @@ -24,6 +25,7 @@ from task.src.config_schedule import ScheduleBuilder from task.src.task_manager import TaskManager from task.tasks import version_check from video.src.constants import VideoTypeEnum +from video.src.index import YoutubeVideo TOPIC = """ @@ -55,6 +57,10 @@ class Command(BaseCommand): self._mig_set_channel_tabs() self._mig_set_video_channel_tabs() self._mig_fix_playlist_description() + self._mig_fix_missing_stats() + self._mig_fix_channel_art_types() + self._mig_fix_channel_description() + self._mig_fix_video_description() def _make_folders(self): """make expected cache folders""" @@ -66,6 +72,7 @@ class Command(BaseCommand): "import", "playlists", "videos", + "ytdlp", ] cache_dir = EnvironmentSettings.CACHE_DIR for folder in folders: @@ -271,134 +278,189 @@ class Command(BaseCommand): def _mig_add_default_playlist_sort(self) -> None: """migrate from 0.5.4 to 0.5.5 set default playlist sortorder""" - self.stdout.write("[MIGRATION] set default playlist sort order") - path = "ta_playlist/_update_by_query" - data = { - "query": { + self._run_migration( + index_name="ta_playlist", + desc="set default playlist sort order", + query={ "bool": { "must_not": [{"exists": {"field": "playlist_sort_order"}}] } }, - "script": { + script={ "source": "ctx._source.playlist_sort_order = 'top'", "lang": "painless", }, - } - response, status_code = ElasticWrap(path).post(data) - if status_code in [200, 201]: - updated = response.get("updated") - if updated: - self.stdout.write( - self.style.SUCCESS(f" ✓ updated {updated} playlists") - ) - else: - self.stdout.write( - self.style.SUCCESS(" no playlists need updating") - ) - return - - message = " 🗙 failed to set default playlist sort order" - self.stdout.write(self.style.ERROR(message)) - self.stdout.write(response) - sleep(60) - raise CommandError(message) + ) def _mig_set_channel_tabs(self) -> None: """migrate from 0.5.4 to 0.5.5 set initial channel tabs""" - self.stdout.write("[MIGRATION] set default channel_tabs") - - path = "ta_channel/_update_by_query" tabs = VideoTypeEnum.values_known() - data = { - "query": { + self._run_migration( + index_name="ta_channel", + desc="set default channel_tabs in channel index", + query={ "bool": {"must_not": [{"exists": {"field": "channel_tabs"}}]} }, - "script": { + script={ "source": f"ctx._source.channel_tabs = {tabs}", "lang": "painless", }, - } - response, status_code = ElasticWrap(path).post(data) - if status_code in [200, 201]: - updated = response.get("updated") - if updated: - self.stdout.write( - self.style.SUCCESS(f" ✓ updated {updated} channels") - ) - else: - self.stdout.write( - self.style.SUCCESS(" no channels need updating") - ) - return - - message = " 🗙 failed to set default channel_tabs" - self.stdout.write(self.style.ERROR(message)) - self.stdout.write(response) - sleep(60) - raise CommandError(message) + ) def _mig_set_video_channel_tabs(self) -> None: """migrate from 0.5.4 to 0.5.5 set initial video channel tabs""" - self.stdout.write("[MIGRATION] set default channel_tabs for videos") - - path = "ta_video/_update_by_query" tabs = VideoTypeEnum.values_known() - data = { - "query": { + self._run_migration( + index_name="ta_video", + desc="set default channel_tabs for videos", + query={ "bool": { "must_not": [{"exists": {"field": "channel.channel_tabs"}}] } }, - "script": { + script={ "source": f"ctx._source.channel.channel_tabs = {tabs}", "lang": "painless", }, - } - response, status_code = ElasticWrap(path).post(data) - if status_code in [200, 201]: - updated = response.get("updated") - if updated: - self.stdout.write( - self.style.SUCCESS(f" ✓ updated {updated} videos") - ) - else: - self.stdout.write( - self.style.SUCCESS(" no videos need updating") - ) - return - - message = " 🗙 failed to set default channel_tabs" - self.stdout.write(self.style.ERROR(message)) - self.stdout.write(response) - sleep(60) - raise CommandError(message) + ) def _mig_fix_playlist_description(self) -> None: """migrate from 0.5.8 to 0.5.9 fix playlist desc null data type""" - self.stdout.write("[MIGRATION] fix playlist description data type") - - path = "ta_playlist/_update_by_query" - data = { - "query": {"term": {"playlist_description": {"value": False}}}, - "script": { - "source": "ctx._source.playlist_description = null", + self._run_migration( + index_name="ta_playlist", + desc="fix playlist description data type", + query={"term": {"playlist_description": {"value": False}}}, + script={ + "source": "ctx._source.remove('playlist_description')", "lang": "painless", }, - } + ) + + def _mig_fix_missing_stats(self) -> None: + """migrate from 0.5.8 to 0.5.9, fix missing stats values""" + fields = [ + "like_count", + "average_rating", + "view_count", + "dislike_count", + ] + for field in fields: + self._run_migration( + index_name="ta_video", + desc=f"fix missing stats field {field}", + query={ + "bool": { + "must_not": [{"exists": {"field": f"stats.{field}"}}] + } + }, + script={ + "source": f"ctx._source.stats.{field} = 0", + "lang": "painless", + }, + ) + + def _mig_fix_channel_art_types(self) -> None: + """migrate from 0.5.8 to 0.5.9, fix channel artwork types""" + fields = [ + "channel_banner_url", + "channel_thumb_url", + "channel_tvart_url", + ] + for field in fields: + self._run_migration( + index_name="ta_channel", + desc=f"fix missing data type for field {field}", + query={"term": {field: {"value": False}}}, + script={ + "source": f"ctx._source.remove('{field}')", + "lang": "painless", + }, + ) + self._run_migration( + index_name="ta_video", + desc=f"fix missing data type for field channel.{field}", + query={"term": {f"channel.{field}": {"value": False}}}, + script={ + "source": f"ctx._source.remove('channel.{field}')", + "lang": "painless", + }, + ) + + def _mig_fix_channel_description(self) -> None: + """migrate from 0.5.8 to 0.5.9, fix channel desc null value""" + desc = "fix channel description null value" + self.stdout.write(f"[MIGRATION] run {desc}") + channels = get_channels( + subscribed_only=False, source=["channel_description", "channel_id"] + ) + counter = 0 + for channel_response in channels: + if not channel_response.get("channel_description") == "": + continue + + channel = YoutubeChannel(youtube_id=channel_response["channel_id"]) + channel.get_from_es() + channel.json_data.pop("channel_description") + channel.upload_to_es() + channel.sync_to_videos() + counter += 1 + + if counter: + suc_msg = f" ✓ updated {counter} channels with videos" + self.stdout.write(self.style.SUCCESS(suc_msg)) + else: + noop_msg = " no items needed updating" + self.stdout.write(self.style.SUCCESS(noop_msg)) + + def _mig_fix_video_description(self) -> None: + """migrate from 0.5.8 to 0.5.9, fix video desc null value""" + desc = "fix video description null value" + self.stdout.write(f"[MIGRATION] run {desc}") + + data = {"_source": ["youtube_id", "description"]} + videos = IndexPaginate("ta_video", data=data).get_results() + + counter = 0 + for video_response in videos: + if not video_response.get("description") == "": + continue + + video = YoutubeVideo(youtube_id=video_response["youtube_id"]) + video.get_from_es() + video.json_data.pop("description") + video.upload_to_es() + + counter += 1 + + if counter: + suc_msg = f" ✓ updated {counter} videos" + self.stdout.write(self.style.SUCCESS(suc_msg)) + else: + noop_msg = " no items needed updating" + self.stdout.write(self.style.SUCCESS(noop_msg)) + + def _run_migration( + self, index_name: str, desc: str, query: dict, script: dict + ): + """run migration""" + self.stdout.write(f"[MIGRATION] run {desc}") + path = f"{index_name}/_update_by_query?wait_for_completion=true" + data = {"query": query, "script": script} response, status_code = ElasticWrap(path).post(data) if status_code in [200, 201]: updated = response.get("updated") if updated: - self.stdout.write( - self.style.SUCCESS(f" ✓ updated {updated} playlists") - ) + suc_msg = f" ✓ updated {updated} docs in {index_name}" + self.stdout.write(self.style.SUCCESS(suc_msg)) + + # ensure index consistency + ElasticWrap(f"{index_name}/_refresh").post() else: - self.stdout.write( - self.style.SUCCESS(" no playlists need updating") - ) + noop_msg = f" no items in {index_name} need updating" + self.stdout.write(self.style.SUCCESS(noop_msg)) return - message = " 🗙 failed to fix playlist description null data type" + message = f" 🗙 failed to run {desc} on index {index_name}" self.stdout.write(self.style.ERROR(message)) self.stdout.write(response) sleep(60) diff --git a/backend/download/serializers.py b/backend/download/serializers.py index bcaf33a9..f5d89058 100644 --- a/backend/download/serializers.py +++ b/backend/download/serializers.py @@ -15,6 +15,7 @@ class DownloadItemSerializer(serializers.Serializer): channel_indexed = serializers.BooleanField() channel_name = serializers.CharField() duration = serializers.CharField() + message = serializers.CharField(required=False) published = serializers.CharField(allow_null=True) status = serializers.ChoiceField( choices=["pending", "ignore"], required=False @@ -24,7 +25,6 @@ class DownloadItemSerializer(serializers.Serializer): vid_thumb_url = serializers.CharField(allow_null=True) vid_type = serializers.ChoiceField(choices=VideoTypeEnum.values()) youtube_id = serializers.CharField() - message = serializers.CharField(required=False) _index = serializers.CharField(required=False) _score = serializers.IntegerField(required=False) diff --git a/backend/download/src/queue.py b/backend/download/src/queue.py index f372c5ea..cb259f82 100644 --- a/backend/download/src/queue.py +++ b/backend/download/src/queue.py @@ -369,16 +369,16 @@ class PendingList(PendingIndex): return None to_add = { - "youtube_id": video_data["id"], - "title": video_data["title"], - "vid_thumb_url": self._extract_thumb(video_data), + "channel_id": video_data["channel_id"], + "channel_indexed": video_data["channel_id"] in self.all_channels, + "channel_name": video_data["channel"], "duration": get_duration_str(video_data.get("duration", 0)), "published": self._extract_published(video_data), "timestamp": int(datetime.now().timestamp()), + "title": video_data["title"], + "vid_thumb_url": self._extract_thumb(video_data), "vid_type": self._extract_vid_type(video_data), - "channel_name": video_data["channel"], - "channel_id": video_data["channel_id"], - "channel_indexed": video_data["channel_id"] in self.all_channels, + "youtube_id": video_data["id"], } serializer = DownloadItemSerializer(data=to_add) is_valid = serializer.is_valid() @@ -406,6 +406,9 @@ class PendingList(PendingIndex): if timestamp and isinstance(timestamp, int): return timestamp + if timestamp and isinstance(timestamp, float): + return int(timestamp) + upload_date = video_data.get("upload_date") if upload_date: try: diff --git a/backend/download/src/thumbnails.py b/backend/download/src/thumbnails.py index 0b6a7c44..87e88e64 100644 --- a/backend/download/src/thumbnails.py +++ b/backend/download/src/thumbnails.py @@ -324,9 +324,9 @@ class ValidatorCallback: """check if all channel artwork is there""" for channel in self.source: urls = ( - channel["_source"]["channel_thumb_url"], - channel["_source"]["channel_banner_url"], - channel["_source"].get("channel_tvart_url", False), + channel["_source"].get("channel_thumb_url"), + channel["_source"].get("channel_banner_url"), + channel["_source"].get("channel_tvart_url"), ) handler = ThumbManager(channel["_source"]["channel_id"]) handler.download_channel_art(urls, skip_existing=True) diff --git a/backend/download/src/yt_dlp_base.py b/backend/download/src/yt_dlp_base.py index 909931c0..9fc0be6a 100644 --- a/backend/download/src/yt_dlp_base.py +++ b/backend/download/src/yt_dlp_base.py @@ -7,9 +7,11 @@ functionality: from datetime import datetime from http import cookiejar from io import StringIO +from os import path import yt_dlp from appsettings.src.config import AppConfig +from common.src.env_settings import EnvironmentSettings from common.src.ta_redis import RedisArchivist from django.conf import settings @@ -23,6 +25,9 @@ class YtWrap: "socket_timeout": 10, "extractor_retries": 3, "retries": 10, + "cachedir": path.abspath( + path.join(EnvironmentSettings.CACHE_DIR, "ytdlp") + ), } def __init__(self, obs_request, config=False): @@ -37,6 +42,7 @@ class YtWrap: if self.config: self._add_cookie() self._add_potoken() + self._add_potoken_url() if getattr(settings, "DEBUG", False): del self.obs["quiet"] @@ -46,7 +52,10 @@ class YtWrap: """add cookie if enabled""" if self.config["downloads"]["cookie_import"]: cookie_io = CookieHandler(self.config).get() - self.obs["cookiefile"] = cookie_io + else: + cookie_io = CookieHandler(self.config).get("cookie_temp") + + self.obs["cookiefile"] = cookie_io def _add_potoken(self): """add potoken if enabled""" @@ -63,6 +72,21 @@ class YtWrap: } ) + def _add_potoken_url(self): + """add bgutils token url""" + if pot_provider_url := self.config["downloads"].get( + "pot_provider_url" + ): + self.obs.update( + { + "extractor_args": { + "youtubepot-bgutilhttp": { + "base_url": [pot_provider_url] + } + } + } + ) + def download(self, url): """make download request""" self.obs.update({"check_formats": "selected"}) @@ -111,26 +135,40 @@ class YtWrap: def _validate_cookie(self): """check cookie and write it back for next use""" if not self.obs.get("cookiefile"): + # empty in tests return - new_cookie = self.obs["cookiefile"].read() - old_cookie = RedisArchivist().get_message_str("cookie") + self.obs["cookiefile"].seek(0) + new_cookie = self.obs["cookiefile"].read().strip("\x00") + + if self.config["downloads"]["cookie_import"]: + cookie_key = "cookie" + expire = False + else: + cookie_key = "cookie_temp" + expire = 60 * 30 # 30 min + + old_cookie = RedisArchivist().get_message_str(cookie_key) if new_cookie and old_cookie != new_cookie: - print("refreshed stored cookie") - RedisArchivist().set_message("cookie", new_cookie, save=True) + print(f"refreshed stored {cookie_key}") + RedisArchivist().set_message( + cookie_key, new_cookie, expire=expire, save=True + ) class CookieHandler: """handle youtube cookie for yt-dlp""" + COOKIE_EMPTY = "# Netscape HTTP Cookie File\n" + def __init__(self, config): self.cookie_io = False self.config = config - def get(self): + def get(self, message_str: str = "cookie"): """get cookie io stream""" - cookie = RedisArchivist().get_message_str("cookie") - self.cookie_io = StringIO(cookie) + cookie = RedisArchivist().get_message_str(message_str) + self.cookie_io = StringIO(cookie or self.COOKIE_EMPTY) return self.cookie_io def set_cookie(self, cookie): diff --git a/backend/download/src/yt_dlp_handler.py b/backend/download/src/yt_dlp_handler.py index 9459ea3b..8aaf0234 100644 --- a/backend/download/src/yt_dlp_handler.py +++ b/backend/download/src/yt_dlp_handler.py @@ -482,10 +482,7 @@ class DownloadPostProcess(DownloaderBase): def embed_metadata(self): """embed metadata in media file""" - meta = self.config["downloads"].get("add_metadata") - thumb = self.config["downloads"].get("add_thumbnail") - - if not meta and not thumb: + if not self.config["downloads"].get("add_metadata"): return queue = RedisQueue(self.VIDEO_QUEUE) diff --git a/backend/download/tests/test_src/test_pending_list.py b/backend/download/tests/test_src/test_pending_list.py index f8656c7c..244fa4f4 100644 --- a/backend/download/tests/test_src/test_pending_list.py +++ b/backend/download/tests/test_src/test_pending_list.py @@ -5,6 +5,12 @@ from datetime import datetime, timezone from download.src.queue import PendingList +def test_returns_scientific_timestamp_if_present(): + video_data = {"timestamp": 1.5135732e9} + result = PendingList._extract_published(video_data) + assert result == 1513573200 + + def test_returns_timestamp_if_present(): video_data = {"timestamp": 1508457600} result = PendingList._extract_published(video_data) diff --git a/backend/playlist/src/index.py b/backend/playlist/src/index.py index 2f96ba8d..9322e01c 100644 --- a/backend/playlist/src/index.py +++ b/backend/playlist/src/index.py @@ -81,10 +81,13 @@ class YoutubePlaylist(YouTubeItem): "playlist_channel": self.youtube_meta["channel"], "playlist_channel_id": self.youtube_meta["channel_id"], "playlist_thumbnail": playlist_thumbnail, - "playlist_description": self.youtube_meta["description"] or None, "playlist_last_refresh": int(datetime.now().timestamp()), "playlist_type": "regular", } + if self.youtube_meta.get("description"): + self.json_data["playlist_description"] = self.youtube_meta[ + "description" + ] def _ensure_channel(self): """make sure channel is indexed""" diff --git a/backend/requirements.txt b/backend/requirements.txt index a310c1f5..6408e29f 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,14 +1,16 @@ -apprise==1.9.6 -celery==5.6.0 -django-auth-ldap==5.2.0 +apprise==1.9.7 +bgutil-ytdlp-pot-provider==1.2.2 +celery==5.6.2 +deepdiff==8.6.1 +django-auth-ldap==5.3.0 django-celery-beat==2.8.1 django-cors-headers==4.9.0 -Django==5.2.9 +Django==5.2.10 djangorestframework==3.16.1 drf-spectacular==0.28.0 # rc:ignore -Pillow==12.0.0 +Pillow==12.1.0 redis==7.1.0 requests==2.32.5 ryd-client==0.0.6 -uvicorn==0.38.0 -yt-dlp[default] @ git+https://github.com/yt-dlp/yt-dlp@468aa6a9b431194949ede9eaad8f33e314a288d6 +uvicorn==0.40.0 +yt-dlp[default] @ git+https://github.com/yt-dlp/yt-dlp@23b846506378a6a9c9a0958382d37f943f7cfa51 diff --git a/backend/user/migrations/0001_initial.py b/backend/user/migrations/0001_initial.py index 7933db76..3b965203 100644 --- a/backend/user/migrations/0001_initial.py +++ b/backend/user/migrations/0001_initial.py @@ -39,7 +39,9 @@ class Migration(migrations.Migration): "is_superuser", models.BooleanField( default=False, - help_text="Designates that this user has all permissions without explicitly assigning them.", + help_text=( + "Designates that this user has all permissions without explicitly assigning them." + ), verbose_name="superuser status", ), ), @@ -49,7 +51,9 @@ class Migration(migrations.Migration): "groups", models.ManyToManyField( blank=True, - help_text="The groups this user belongs to. A user will get all permissions granted to each of their groups.", + help_text=( + "The groups this user belongs to. A user will get all permissions granted to each of their groups." + ), related_name="user_set", related_query_name="user", to="auth.group", diff --git a/backend/video/serializers.py b/backend/video/serializers.py index 1cdf167c..78de3464 100644 --- a/backend/video/serializers.py +++ b/backend/video/serializers.py @@ -16,6 +16,7 @@ class PlayerSerializer(serializers.Serializer): watched_date = serializers.IntegerField(required=False) duration = serializers.IntegerField() duration_str = serializers.CharField() + progress = serializers.FloatField(required=False) position = serializers.FloatField(required=False) @@ -44,49 +45,49 @@ class SponsorBlockSerializer(serializers.Serializer): class StatsSerializer(serializers.Serializer): """serialize stats""" - like_count = serializers.IntegerField(required=False) - average_rating = serializers.FloatField(required=False) - view_count = serializers.IntegerField(required=False) - dislike_count = serializers.IntegerField(required=False) + like_count = serializers.IntegerField() + average_rating = serializers.FloatField() + view_count = serializers.IntegerField() + dislike_count = serializers.IntegerField() class StreamItemSerializer(serializers.Serializer): """serialize stream item""" - index = serializers.IntegerField() - codec = serializers.CharField() bitrate = serializers.IntegerField() + codec = serializers.CharField() + height = serializers.IntegerField(required=False) + index = serializers.IntegerField() type = serializers.ChoiceField(choices=["video", "audio"]) width = serializers.IntegerField(required=False) - height = serializers.IntegerField(required=False) class SubtitleFragmentSerializer(serializers.Serializer): """serialize subtitle fragment""" - subtitle_index = serializers.IntegerField() - subtitle_line = serializers.CharField() - subtitle_start = serializers.CharField() - subtitle_fragment_id = serializers.CharField() - subtitle_end = serializers.CharField() - youtube_id = serializers.CharField() - title = serializers.CharField() subtitle_channel = serializers.CharField() subtitle_channel_id = serializers.CharField() - subtitle_last_refresh = serializers.IntegerField() + subtitle_end = serializers.CharField() + subtitle_fragment_id = serializers.CharField() + subtitle_index = serializers.IntegerField() subtitle_lang = serializers.CharField() + subtitle_last_refresh = serializers.IntegerField() + subtitle_line = serializers.CharField() subtitle_source = serializers.ChoiceField(choices=["user", "auto"]) + subtitle_start = serializers.CharField() + title = serializers.CharField() + youtube_id = serializers.CharField() class SubtitleItemSerializer(serializers.Serializer): """serialize subtitle item""" - ext = serializers.ChoiceField(choices=["json3"]) - name = serializers.CharField() - source = serializers.ChoiceField(choices=["user", "auto"]) + ext = serializers.ChoiceField(choices=["json3", "vtt"]) lang = serializers.CharField() media_url = serializers.CharField() - url = serializers.URLField() + name = serializers.CharField() + source = serializers.ChoiceField(choices=["user", "auto"]) + url = serializers.URLField(allow_null=True) class VideoSerializer(serializers.Serializer): @@ -97,7 +98,7 @@ class VideoSerializer(serializers.Serializer): channel = ChannelSerializer(required=False) comment_count = serializers.IntegerField(allow_null=True, required=False) date_downloaded = serializers.IntegerField() - description = serializers.CharField() + description = serializers.CharField(allow_null=True) media_size = serializers.IntegerField() media_url = serializers.CharField() player = PlayerSerializer() @@ -146,17 +147,18 @@ class VideoListQuerySerializer(serializers.Serializer): class CommentItemSerializer(serializers.Serializer): """serialize comment item""" - comment_id = serializers.CharField() - comment_text = serializers.CharField() - comment_timestamp = serializers.IntegerField() - comment_time_text = serializers.CharField() - comment_likecount = serializers.IntegerField() - comment_is_favorited = serializers.BooleanField() comment_author = serializers.CharField() comment_author_id = serializers.CharField() - comment_author_thumbnail = serializers.URLField() comment_author_is_uploader = serializers.BooleanField() + comment_author_thumbnail = serializers.URLField() + comment_id = serializers.CharField() + comment_is_favorited = serializers.BooleanField() + comment_likecount = serializers.IntegerField() comment_parent = serializers.CharField() + comment_text = serializers.CharField() + comment_time_text = serializers.CharField() + comment_timestamp = serializers.IntegerField() + comment_replies = serializers.SerializerMethodField() @extend_schema_field(serializers.ListField()) @@ -170,10 +172,10 @@ class CommentItemSerializer(serializers.Serializer): class CommentsSerializer(serializers.Serializer): """serialize comments as indexed""" - youtube_id = serializers.CharField() - comment_last_refresh = serializers.IntegerField() comment_channel_id = serializers.CharField() comment_comments = CommentItemSerializer(many=True) + comment_last_refresh = serializers.IntegerField() + youtube_id = serializers.CharField() class PlaylistNavMetaSerializer(serializers.Serializer): diff --git a/backend/video/src/comments.py b/backend/video/src/comments.py index 368b5b0b..5aeff6ba 100644 --- a/backend/video/src/comments.py +++ b/backend/video/src/comments.py @@ -39,10 +39,10 @@ class Comments: self.format_comments(comments_raw) self.json_data = { - "youtube_id": self.youtube_id, - "comment_last_refresh": int(datetime.now().timestamp()), "comment_channel_id": channel_id, "comment_comments": self.comments_format, + "comment_last_refresh": int(datetime.now().timestamp()), + "youtube_id": self.youtube_id, } if upload: self.upload_comments() @@ -124,20 +124,20 @@ class Comments: if not comment.get("author"): comment["author"] = comment.get("author_id", "Unknown") + is_uploader = comment.get("author_is_uploader", False) + cleaned_comment = { - "comment_id": comment["id"], - "comment_text": comment["text"].replace("\xa0", ""), - "comment_timestamp": comment["timestamp"], - "comment_time_text": time_text, - "comment_likecount": comment.get("like_count", None), - "comment_is_favorited": comment.get("is_favorited", False), "comment_author": comment["author"], "comment_author_id": comment["author_id"], + "comment_author_is_uploader": is_uploader, "comment_author_thumbnail": comment["author_thumbnail"], - "comment_author_is_uploader": comment.get( - "author_is_uploader", False - ), + "comment_id": comment["id"], + "comment_is_favorited": comment.get("is_favorited", False), + "comment_likecount": comment.get("like_count", None), "comment_parent": comment["parent"], + "comment_text": comment["text"].replace("\xa0", ""), + "comment_time_text": time_text, + "comment_timestamp": comment["timestamp"], } return cleaned_comment diff --git a/backend/video/src/index.py b/backend/video/src/index.py index e039aaa8..5226e651 100644 --- a/backend/video/src/index.py +++ b/backend/video/src/index.py @@ -198,31 +198,33 @@ class YoutubeVideo(YouTubeItem, YoutubeSubtitle): def process_youtube_meta(self): """extract relevant fields from youtube""" self._validate_id() - # extract self.channel_id = self.youtube_meta["channel_id"] last_refresh = int(datetime.now().timestamp()) - # build json_data basics self.json_data = { - "title": self.youtube_meta["title"], - "description": self.youtube_meta.get("description", ""), - "category": self.youtube_meta.get("categories", []), - "vid_thumb_url": self.youtube_meta["thumbnail"], - "tags": self.youtube_meta.get("tags", []), - "published": self._build_published(), - "vid_last_refresh": last_refresh, - "date_downloaded": last_refresh, - "youtube_id": self.youtube_id, - # Using .value to make json encodable - "vid_type": self.video_type.value, "active": True, + "category": self.youtube_meta.get("categories", []), + "date_downloaded": last_refresh, + "published": self._build_published(), + "tags": self.youtube_meta.get("tags", []), + "title": self.youtube_meta["title"], + "vid_last_refresh": last_refresh, + "vid_thumb_url": self.youtube_meta["thumbnail"], + "vid_type": self.video_type.value, + "youtube_id": self.youtube_id, } - def _build_published(self): + if description := self.youtube_meta.get("description"): + self.json_data["description"] = description + + def _build_published(self) -> int | str: """build published date or timestamp""" timestamp = self.youtube_meta.get("timestamp") if timestamp and isinstance(timestamp, int): return timestamp + if timestamp and isinstance(timestamp, float): + return int(timestamp) + upload_date = self.youtube_meta["upload_date"] if not upload_date: raise ValueError( @@ -255,10 +257,10 @@ class YoutubeVideo(YouTubeItem, YoutubeSubtitle): def _add_stats(self): """add stats dicst to json_data""" stats = { - "view_count": self.youtube_meta.get("view_count", 0), - "like_count": self.youtube_meta.get("like_count", 0), - "dislike_count": self.youtube_meta.get("dislike_count", 0), - "average_rating": self.youtube_meta.get("average_rating", 0), + "view_count": self.youtube_meta.get("view_count") or 0, + "like_count": self.youtube_meta.get("like_count") or 0, + "dislike_count": self.youtube_meta.get("dislike_count") or 0, + "average_rating": self.youtube_meta.get("average_rating") or 0, } self.json_data.update({"stats": stats}) @@ -288,9 +290,9 @@ class YoutubeVideo(YouTubeItem, YoutubeSubtitle): self.json_data.update( { "player": { - "watched": False, "duration": duration, "duration_str": get_duration_str(duration), + "watched": False, } } ) @@ -379,8 +381,8 @@ class YoutubeVideo(YouTubeItem, YoutubeSubtitle): return dislikes = { - "dislike_count": result.get("dislikes", 0), - "average_rating": result.get("rating", 0), + "dislike_count": result.get("dislikes") or 0, + "average_rating": result.get("rating") or 0, } self.json_data["stats"].update(dislikes) @@ -412,7 +414,7 @@ class YoutubeVideo(YouTubeItem, YoutubeSubtitle): subtitle_media_url = f"{base_name}.{lang}.vtt" to_add = { "ext": "vtt", - "url": False, + "url": None, "name": lang, "lang": lang, "source": "file", @@ -433,8 +435,6 @@ class YoutubeVideo(YouTubeItem, YoutubeSubtitle): if self.config["downloads"].get("add_metadata"): self._embed_text_data() - - if self.config["downloads"].get("add_thumbnail"): self._embed_artwork() def _embed_text_data(self): diff --git a/backend/video/src/media_streams.py b/backend/video/src/media_streams.py index 0d8c1823..93e1f983 100644 --- a/backend/video/src/media_streams.py +++ b/backend/video/src/media_streams.py @@ -12,7 +12,7 @@ class MediaStreamExtractor: self.media_path = media_path self.metadata = [] - def extract_metadata(self): + def extract_metadata(self) -> list[dict]: """entry point to extract metadata""" cmd = [ @@ -38,17 +38,15 @@ class MediaStreamExtractor: return self.metadata - def process_stream(self, stream): + def process_stream(self, stream) -> None: """parse stream to metadata""" codec_type = stream.get("codec_type") if codec_type == "video": self._extract_video_metadata(stream) elif codec_type == "audio": self._extract_audio_metadata(stream) - else: - return - def _extract_video_metadata(self, stream): + def _extract_video_metadata(self, stream) -> None: """parse video metadata""" if "bit_rate" not in stream: # is probably thumbnail @@ -56,26 +54,26 @@ class MediaStreamExtractor: self.metadata.append( { - "type": "video", - "index": stream["index"], + "bitrate": int(stream.get("bit_rate", 0)), "codec": stream["codec_name"], - "width": stream["width"], "height": stream["height"], - "bitrate": int(stream["bit_rate"]), + "index": stream["index"], + "type": "video", + "width": stream["width"], } ) - def _extract_audio_metadata(self, stream): + def _extract_audio_metadata(self, stream) -> None: """extract audio metadata""" self.metadata.append( { - "type": "audio", - "index": stream["index"], - "codec": stream.get("codec_name", "undefined"), "bitrate": int(stream.get("bit_rate", 0)), + "codec": stream.get("codec_name", "undefined"), + "index": stream["index"], + "type": "audio", } ) - def get_file_size(self): + def get_file_size(self) -> int: """get filesize in bytes""" return stat(self.media_path).st_size diff --git a/backend/video/src/meta_embed.py b/backend/video/src/meta_embed.py index 21f9b3f7..fb8bcc33 100644 --- a/backend/video/src/meta_embed.py +++ b/backend/video/src/meta_embed.py @@ -44,7 +44,7 @@ class MetadataEmbed: paginate = IndexPaginate( index_name=self.INDEX_NAME, data=data, - size=200, + size=100, callback=MetadataEmbedCallback, task=self.task, total=self._get_total(), diff --git a/backend/video/src/subtitle.py b/backend/video/src/subtitle.py index 68868eb9..1fdd0919 100644 --- a/backend/video/src/subtitle.py +++ b/backend/video/src/subtitle.py @@ -149,7 +149,16 @@ class YoutubeSubtitle: query_str = parser.create_bulk_import(documents) self.index_subtitle(query_str) - indexed.append(subtitle) + indexed.append( + { + "ext": "json3", + "name": subtitle["name"], + "source": subtitle["source"], + "lang": subtitle["lang"], + "media_url": subtitle["media_url"], + "url": subtitle["url"], + } + ) rand_sleep(self.video.config) return indexed @@ -336,13 +345,13 @@ class SubtitleParser: documents = self._chunk_list(video.youtube_id) channel = video.json_data.get("channel") meta_dict = { - "youtube_id": video.youtube_id, - "title": video.json_data.get("title"), "subtitle_channel": channel.get("channel_name"), "subtitle_channel_id": channel.get("channel_id"), - "subtitle_last_refresh": int(datetime.now().timestamp()), "subtitle_lang": self.lang, + "subtitle_last_refresh": int(datetime.now().timestamp()), "subtitle_source": source, + "title": video.json_data.get("title"), + "youtube_id": video.youtube_id, } _ = [i.update(meta_dict) for i in documents] diff --git a/frontend/.nvmrc b/frontend/.nvmrc new file mode 100644 index 00000000..1d9b7831 --- /dev/null +++ b/frontend/.nvmrc @@ -0,0 +1 @@ +22.12.0 diff --git a/frontend/package-lock.json b/frontend/package-lock.json index aea629f1..135b0bee 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,37 +9,37 @@ "version": "0.5.1", "dependencies": { "dompurify": "^3.3.1", - "react": "^19.2.1", - "react-dom": "^19.2.1", - "react-router-dom": "^7.10.1", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "react-router-dom": "^7.11.0", "zustand": "^5.0.9" }, "devDependencies": { "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", - "@typescript-eslint/eslint-plugin": "^8.39.0", - "@typescript-eslint/parser": "^8.39.0", + "@typescript-eslint/eslint-plugin": "^8.51.0", + "@typescript-eslint/parser": "^8.51.0", "@vitejs/plugin-react-swc": "^4.2.2", - "eslint": "^9.39.1", + "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.24", - "globals": "^16.5.0", + "eslint-plugin-react-refresh": "^0.4.26", + "globals": "^17.0.0", "prettier": "3.7.4", "typescript": "^5.9.3", - "typescript-eslint": "^8.49.0", - "vite": ">=7.2.7", + "typescript-eslint": "^8.51.0", + "vite": ">=7.3.0", "vite-plugin-checker": "^0.12.0" } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -48,9 +48,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", - "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", "dev": true, "license": "MIT", "engines": { @@ -58,21 +58,21 @@ } }, "node_modules/@babel/core": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", - "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", @@ -99,14 +99,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", - "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.5", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -116,13 +116,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.27.2", + "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", @@ -153,29 +153,29 @@ } }, "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -215,27 +215,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", - "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.5" + "@babel/types": "^7.28.6" }, "bin": { "parser": "bin/babel-parser.js" @@ -245,33 +245,33 @@ } }, "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", - "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.5", + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.5", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.5", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", "debug": "^4.3.1" }, "engines": { @@ -279,9 +279,9 @@ } }, "node_modules/@babel/types": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", - "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", "dev": true, "license": "MIT", "dependencies": { @@ -293,9 +293,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", - "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz", + "integrity": "sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==", "cpu": [ "ppc64" ], @@ -310,9 +310,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", - "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.2.tgz", + "integrity": "sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==", "cpu": [ "arm" ], @@ -327,9 +327,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", - "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz", + "integrity": "sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==", "cpu": [ "arm64" ], @@ -344,9 +344,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", - "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.2.tgz", + "integrity": "sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==", "cpu": [ "x64" ], @@ -361,9 +361,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", - "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz", + "integrity": "sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==", "cpu": [ "arm64" ], @@ -378,9 +378,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", - "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz", + "integrity": "sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==", "cpu": [ "x64" ], @@ -395,9 +395,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", - "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz", + "integrity": "sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==", "cpu": [ "arm64" ], @@ -412,9 +412,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", - "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz", + "integrity": "sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==", "cpu": [ "x64" ], @@ -429,9 +429,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", - "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz", + "integrity": "sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==", "cpu": [ "arm" ], @@ -446,9 +446,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", - "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz", + "integrity": "sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==", "cpu": [ "arm64" ], @@ -463,9 +463,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", - "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz", + "integrity": "sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==", "cpu": [ "ia32" ], @@ -480,9 +480,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", - "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz", + "integrity": "sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==", "cpu": [ "loong64" ], @@ -497,9 +497,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", - "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz", + "integrity": "sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==", "cpu": [ "mips64el" ], @@ -514,9 +514,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", - "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz", + "integrity": "sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==", "cpu": [ "ppc64" ], @@ -531,9 +531,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", - "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz", + "integrity": "sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==", "cpu": [ "riscv64" ], @@ -548,9 +548,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", - "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz", + "integrity": "sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==", "cpu": [ "s390x" ], @@ -565,9 +565,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", - "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz", + "integrity": "sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==", "cpu": [ "x64" ], @@ -582,9 +582,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", - "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz", + "integrity": "sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==", "cpu": [ "arm64" ], @@ -599,9 +599,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", - "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz", + "integrity": "sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==", "cpu": [ "x64" ], @@ -616,9 +616,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", - "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz", + "integrity": "sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==", "cpu": [ "arm64" ], @@ -633,9 +633,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", - "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz", + "integrity": "sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==", "cpu": [ "x64" ], @@ -650,9 +650,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", - "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz", + "integrity": "sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==", "cpu": [ "arm64" ], @@ -667,9 +667,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", - "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz", + "integrity": "sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==", "cpu": [ "x64" ], @@ -684,9 +684,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", - "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz", + "integrity": "sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==", "cpu": [ "arm64" ], @@ -701,9 +701,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", - "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz", + "integrity": "sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==", "cpu": [ "ia32" ], @@ -718,9 +718,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", - "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz", + "integrity": "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==", "cpu": [ "x64" ], @@ -735,9 +735,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -829,9 +829,9 @@ } }, "node_modules/@eslint/eslintrc": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", - "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz", + "integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==", "dev": true, "license": "MIT", "dependencies": { @@ -841,7 +841,7 @@ "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", + "js-yaml": "^4.1.1", "minimatch": "^3.1.2", "strip-json-comments": "^3.1.1" }, @@ -900,9 +900,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", - "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.2.tgz", + "integrity": "sha512-q1mjIoW1VX4IvSocvM/vbTiveKC4k9eLrajNEuSsmjymSDEbpGddtpfOoN7YGAqBK3NG+uqo8ia4PDTt8buCYA==", "dev": true, "license": "MIT", "engines": { @@ -1046,9 +1046,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", - "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.56.0.tgz", + "integrity": "sha512-LNKIPA5k8PF1+jAFomGe3qN3bbIgJe/IlpDBwuVjrDKrJhVWywgnJvflMt/zkbVNLFtF1+94SljYQS6e99klnw==", "cpu": [ "arm" ], @@ -1060,9 +1060,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", - "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.56.0.tgz", + "integrity": "sha512-lfbVUbelYqXlYiU/HApNMJzT1E87UPGvzveGg2h0ktUNlOCxKlWuJ9jtfvs1sKHdwU4fzY7Pl8sAl49/XaEk6Q==", "cpu": [ "arm64" ], @@ -1074,9 +1074,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", - "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.56.0.tgz", + "integrity": "sha512-EgxD1ocWfhoD6xSOeEEwyE7tDvwTgZc8Bss7wCWe+uc7wO8G34HHCUH+Q6cHqJubxIAnQzAsyUsClt0yFLu06w==", "cpu": [ "arm64" ], @@ -1088,9 +1088,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", - "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.56.0.tgz", + "integrity": "sha512-1vXe1vcMOssb/hOF8iv52A7feWW2xnu+c8BV4t1F//m9QVLTfNVpEdja5ia762j/UEJe2Z1jAmEqZAK42tVW3g==", "cpu": [ "x64" ], @@ -1102,9 +1102,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", - "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.56.0.tgz", + "integrity": "sha512-bof7fbIlvqsyv/DtaXSck4VYQ9lPtoWNFCB/JY4snlFuJREXfZnm+Ej6yaCHfQvofJDXLDMTVxWscVSuQvVWUQ==", "cpu": [ "arm64" ], @@ -1116,9 +1116,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", - "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.56.0.tgz", + "integrity": "sha512-KNa6lYHloW+7lTEkYGa37fpvPq+NKG/EHKM8+G/g9WDU7ls4sMqbVRV78J6LdNuVaeeK5WB9/9VAFbKxcbXKYg==", "cpu": [ "x64" ], @@ -1130,9 +1130,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", - "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.56.0.tgz", + "integrity": "sha512-E8jKK87uOvLrrLN28jnAAAChNq5LeCd2mGgZF+fGF5D507WlG/Noct3lP/QzQ6MrqJ5BCKNwI9ipADB6jyiq2A==", "cpu": [ "arm" ], @@ -1144,9 +1144,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", - "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.56.0.tgz", + "integrity": "sha512-jQosa5FMYF5Z6prEpTCCmzCXz6eKr/tCBssSmQGEeozA9tkRUty/5Vx06ibaOP9RCrW1Pvb8yp3gvZhHwTDsJw==", "cpu": [ "arm" ], @@ -1158,9 +1158,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", - "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.56.0.tgz", + "integrity": "sha512-uQVoKkrC1KGEV6udrdVahASIsaF8h7iLG0U0W+Xn14ucFwi6uS539PsAr24IEF9/FoDtzMeeJXJIBo5RkbNWvQ==", "cpu": [ "arm64" ], @@ -1172,9 +1172,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", - "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.56.0.tgz", + "integrity": "sha512-vLZ1yJKLxhQLFKTs42RwTwa6zkGln+bnXc8ueFGMYmBTLfNu58sl5/eXyxRa2RarTkJbXl8TKPgfS6V5ijNqEA==", "cpu": [ "arm64" ], @@ -1186,9 +1186,23 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", - "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.56.0.tgz", + "integrity": "sha512-FWfHOCub564kSE3xJQLLIC/hbKqHSVxy8vY75/YHHzWvbJL7aYJkdgwD/xGfUlL5UV2SB7otapLrcCj2xnF1dg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.56.0.tgz", + "integrity": "sha512-z1EkujxIh7nbrKL1lmIpqFTc/sr0u8Uk0zK/qIEFldbt6EDKWFk/pxFq3gYj4Bjn3aa9eEhYRlL3H8ZbPT1xvA==", "cpu": [ "loong64" ], @@ -1200,9 +1214,23 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", - "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.56.0.tgz", + "integrity": "sha512-iNFTluqgdoQC7AIE8Q34R3AuPrJGJirj5wMUErxj22deOcY7XwZRaqYmB6ZKFHoVGqRcRd0mqO+845jAibKCkw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.56.0.tgz", + "integrity": "sha512-MtMeFVlD2LIKjp2sE2xM2slq3Zxf9zwVuw0jemsxvh1QOpHSsSzfNOTH9uYW9i1MXFxUSMmLpeVeUzoNOKBaWg==", "cpu": [ "ppc64" ], @@ -1214,9 +1242,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", - "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.56.0.tgz", + "integrity": "sha512-in+v6wiHdzzVhYKXIk5U74dEZHdKN9KH0Q4ANHOTvyXPG41bajYRsy7a8TPKbYPl34hU7PP7hMVHRvv/5aCSew==", "cpu": [ "riscv64" ], @@ -1228,9 +1256,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", - "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.56.0.tgz", + "integrity": "sha512-yni2raKHB8m9NQpI9fPVwN754mn6dHQSbDTwxdr9SE0ks38DTjLMMBjrwvB5+mXrX+C0npX0CVeCUcvvvD8CNQ==", "cpu": [ "riscv64" ], @@ -1242,9 +1270,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", - "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.56.0.tgz", + "integrity": "sha512-zhLLJx9nQPu7wezbxt2ut+CI4YlXi68ndEve16tPc/iwoylWS9B3FxpLS2PkmfYgDQtosah07Mj9E0khc3Y+vQ==", "cpu": [ "s390x" ], @@ -1256,9 +1284,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", - "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.56.0.tgz", + "integrity": "sha512-MVC6UDp16ZSH7x4rtuJPAEoE1RwS8N4oK9DLHy3FTEdFoUTCFVzMfJl/BVJ330C+hx8FfprA5Wqx4FhZXkj2Kw==", "cpu": [ "x64" ], @@ -1270,9 +1298,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", - "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.56.0.tgz", + "integrity": "sha512-ZhGH1eA4Qv0lxaV00azCIS1ChedK0V32952Md3FtnxSqZTBTd6tgil4nZT5cU8B+SIw3PFYkvyR4FKo2oyZIHA==", "cpu": [ "x64" ], @@ -1283,10 +1311,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.56.0.tgz", + "integrity": "sha512-O16XcmyDeFI9879pEcmtWvD/2nyxR9mF7Gs44lf1vGGx8Vg2DRNx11aVXBEqOQhWb92WN4z7fW/q4+2NYzCbBA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", - "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.56.0.tgz", + "integrity": "sha512-LhN/Reh+7F3RCgQIRbgw8ZMwUwyqJM+8pXNT6IIJAqm2IdKkzpCh/V9EdgOMBKuebIrzswqy4ATlrDgiOwbRcQ==", "cpu": [ "arm64" ], @@ -1298,9 +1340,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", - "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.56.0.tgz", + "integrity": "sha512-kbFsOObXp3LBULg1d3JIUQMa9Kv4UitDmpS+k0tinPBz3watcUiV2/LUDMMucA6pZO3WGE27P7DsfaN54l9ing==", "cpu": [ "arm64" ], @@ -1312,9 +1354,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", - "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.56.0.tgz", + "integrity": "sha512-vSSgny54D6P4vf2izbtFm/TcWYedw7f8eBrOiGGecyHyQB9q4Kqentjaj8hToe+995nob/Wv48pDqL5a62EWtg==", "cpu": [ "ia32" ], @@ -1326,9 +1368,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", - "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.56.0.tgz", + "integrity": "sha512-FeCnkPCTHQJFbiGG49KjV5YGW/8b9rrXAM2Mz2kiIoktq2qsJxRD5giEMEOD2lPdgs72upzefaUvS+nc8E3UzQ==", "cpu": [ "x64" ], @@ -1340,9 +1382,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", - "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.56.0.tgz", + "integrity": "sha512-H8AE9Ur/t0+1VXujj90w0HrSOuv0Nq9r1vSZF2t5km20NTfosQsGGUXDaKdQZzwuLts7IyL1fYT4hM95TI9c4g==", "cpu": [ "x64" ], @@ -1354,9 +1396,9 @@ ] }, "node_modules/@swc/core": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.2.tgz", - "integrity": "sha512-OQm+yJdXxvSjqGeaWhP6Ia264ogifwAO7Q12uTDVYj/Ks4jBTI4JknlcjDRAXtRhqbWsfbZyK/5RtuIPyptk3w==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.15.10.tgz", + "integrity": "sha512-udNofxftduMUEv7nqahl2nvodCiCDQ4Ge0ebzsEm6P8s0RC2tBM0Hqx0nNF5J/6t9uagFJyWIDjXy3IIWMHDJw==", "dev": true, "hasInstallScript": true, "license": "Apache-2.0", @@ -1372,16 +1414,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.15.2", - "@swc/core-darwin-x64": "1.15.2", - "@swc/core-linux-arm-gnueabihf": "1.15.2", - "@swc/core-linux-arm64-gnu": "1.15.2", - "@swc/core-linux-arm64-musl": "1.15.2", - "@swc/core-linux-x64-gnu": "1.15.2", - "@swc/core-linux-x64-musl": "1.15.2", - "@swc/core-win32-arm64-msvc": "1.15.2", - "@swc/core-win32-ia32-msvc": "1.15.2", - "@swc/core-win32-x64-msvc": "1.15.2" + "@swc/core-darwin-arm64": "1.15.10", + "@swc/core-darwin-x64": "1.15.10", + "@swc/core-linux-arm-gnueabihf": "1.15.10", + "@swc/core-linux-arm64-gnu": "1.15.10", + "@swc/core-linux-arm64-musl": "1.15.10", + "@swc/core-linux-x64-gnu": "1.15.10", + "@swc/core-linux-x64-musl": "1.15.10", + "@swc/core-win32-arm64-msvc": "1.15.10", + "@swc/core-win32-ia32-msvc": "1.15.10", + "@swc/core-win32-x64-msvc": "1.15.10" }, "peerDependencies": { "@swc/helpers": ">=0.5.17" @@ -1393,9 +1435,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.2.tgz", - "integrity": "sha512-Ghyz4RJv4zyXzrUC1B2MLQBbppIB5c4jMZJybX2ebdEQAvryEKp3gq1kBksCNsatKGmEgXul88SETU19sMWcrw==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.15.10.tgz", + "integrity": "sha512-U72pGqmJYbjrLhMndIemZ7u9Q9owcJczGxwtfJlz/WwMaGYAV/g4nkGiUVk/+QSX8sFCAjanovcU1IUsP2YulA==", "cpu": [ "arm64" ], @@ -1410,9 +1452,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.2.tgz", - "integrity": "sha512-7n/PGJOcL2QoptzL42L5xFFfXY5rFxLHnuz1foU+4ruUTG8x2IebGhtwVTpaDN8ShEv2UZObBlT1rrXTba15Zw==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.15.10.tgz", + "integrity": "sha512-NZpDXtwHH083L40xdyj1sY31MIwLgOxKfZEAGCI8xHXdHa+GWvEiVdGiu4qhkJctoHFzAEc7ZX3GN5phuJcPuQ==", "cpu": [ "x64" ], @@ -1427,9 +1469,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.2.tgz", - "integrity": "sha512-ZUQVCfRJ9wimuxkStRSlLwqX4TEDmv6/J+E6FicGkQ6ssLMWoKDy0cAo93HiWt/TWEee5vFhFaSQYzCuBEGO6A==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.15.10.tgz", + "integrity": "sha512-ioieF5iuRziUF1HkH1gg1r93e055dAdeBAPGAk40VjqpL5/igPJ/WxFHGvc6WMLhUubSJI4S0AiZAAhEAp1jDg==", "cpu": [ "arm" ], @@ -1444,9 +1486,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.2.tgz", - "integrity": "sha512-GZh3pYBmfnpQ+JIg+TqLuz+pM+Mjsk5VOzi8nwKn/m+GvQBsxD5ectRtxuWUxMGNG8h0lMy4SnHRqdK3/iJl7A==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.15.10.tgz", + "integrity": "sha512-tD6BClOrxSsNus9cJL7Gxdv7z7Y2hlyvZd9l0NQz+YXzmTWqnfzLpg16ovEI7gknH2AgDBB5ywOsqu8hUgSeEQ==", "cpu": [ "arm64" ], @@ -1461,9 +1503,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.2.tgz", - "integrity": "sha512-5av6VYZZeneiYIodwzGMlnyVakpuYZryGzFIbgu1XP8wVylZxduEzup4eP8atiMDFmIm+s4wn8GySJmYqeJC0A==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.15.10.tgz", + "integrity": "sha512-4uAHO3nbfbrTcmO/9YcVweTQdx5fN3l7ewwl5AEK4yoC4wXmoBTEPHAVdKNe4r9+xrTgd4BgyPsy0409OjjlMw==", "cpu": [ "arm64" ], @@ -1478,9 +1520,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.2.tgz", - "integrity": "sha512-1nO/UfdCLuT/uE/7oB3EZgTeZDCIa6nL72cFEpdegnqpJVNDI6Qb8U4g/4lfVPkmHq2lvxQ0L+n+JdgaZLhrRA==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.15.10.tgz", + "integrity": "sha512-W0h9ONNw1pVIA0cN7wtboOSTl4Jk3tHq+w2cMPQudu9/+3xoCxpFb9ZdehwCAk29IsvdWzGzY6P7dDVTyFwoqg==", "cpu": [ "x64" ], @@ -1495,9 +1537,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.2.tgz", - "integrity": "sha512-Ksfrb0Tx310kr+TLiUOvB/I80lyZ3lSOp6cM18zmNRT/92NB4mW8oX2Jo7K4eVEI2JWyaQUAFubDSha2Q+439A==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.15.10.tgz", + "integrity": "sha512-XQNZlLZB62S8nAbw7pqoqwy91Ldy2RpaMRqdRN3T+tAg6Xg6FywXRKCsLh6IQOadr4p1+lGnqM/Wn35z5a/0Vw==", "cpu": [ "x64" ], @@ -1512,9 +1554,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.2.tgz", - "integrity": "sha512-IzUb5RlMUY0r1A9IuJrQ7Tbts1wWb73/zXVXT8VhewbHGoNlBKE0qUhKMED6Tv4wDF+pmbtUJmKXDthytAvLmg==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.15.10.tgz", + "integrity": "sha512-qnAGrRv5Nj/DATxAmCnJQRXXQqnJwR0trxLndhoHoxGci9MuguNIjWahS0gw8YZFjgTinbTxOwzatkoySihnmw==", "cpu": [ "arm64" ], @@ -1529,9 +1571,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.2.tgz", - "integrity": "sha512-kCATEzuY2LP9AlbU2uScjcVhgnCAkRdu62vbce17Ro5kxEHxYWcugkveyBRS3AqZGtwAKYbMAuNloer9LS/hpw==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.15.10.tgz", + "integrity": "sha512-i4X/q8QSvzVlaRtv1xfnfl+hVKpCfiJ+9th484rh937fiEZKxZGf51C+uO0lfKDP1FfnT6C1yBYwHy7FLBVXFw==", "cpu": [ "ia32" ], @@ -1546,9 +1588,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.15.2", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.2.tgz", - "integrity": "sha512-iJaHeYCF4jTn7OEKSa3KRiuVFIVYts8jYjNmCdyz1u5g8HRyTDISD76r8+ljEOgm36oviRQvcXaw6LFp1m0yyA==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.15.10.tgz", + "integrity": "sha512-HvY8XUFuoTXn6lSccDLYFlXv1SU/PzYi4PyUqGT++WfTnbw/68N/7BdUZqglGRwiSqr0qhYt/EhmBpULj0J9rA==", "cpu": [ "x64" ], @@ -1594,9 +1636,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.2.7", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", - "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "version": "19.2.9", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.9.tgz", + "integrity": "sha512-Lpo8kgb/igvMIPeNV2rsYKTgaORYdO1XGVZ4Qz3akwOj0ySGYMPlQWa8BaLn0G63D1aSaAQ5ldR06wCpChQCjA==", "devOptional": true, "license": "MIT", "dependencies": { @@ -1621,20 +1663,20 @@ "optional": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.49.0.tgz", - "integrity": "sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==", + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.53.1.tgz", + "integrity": "sha512-cFYYFZ+oQFi6hUnBTbLRXfTJiaQtYE3t4O692agbBl+2Zy+eqSKWtPjhPXJu1G7j4RLjKgeJPDdq3EqOwmX5Ag==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.49.0", - "@typescript-eslint/type-utils": "8.49.0", - "@typescript-eslint/utils": "8.49.0", - "@typescript-eslint/visitor-keys": "8.49.0", - "ignore": "^7.0.0", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.53.1", + "@typescript-eslint/type-utils": "8.53.1", + "@typescript-eslint/utils": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1644,23 +1686,23 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.49.0", + "@typescript-eslint/parser": "^8.53.1", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.49.0.tgz", - "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==", + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.53.1.tgz", + "integrity": "sha512-nm3cvFN9SqZGXjmw5bZ6cGmvJSyJPn0wU9gHAZZHDnZl2wF9PhHv78Xf06E0MaNk4zLVHL8hb2/c32XvyJOLQg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.49.0", - "@typescript-eslint/types": "8.49.0", - "@typescript-eslint/typescript-estree": "8.49.0", - "@typescript-eslint/visitor-keys": "8.49.0", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.53.1", + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1675,15 +1717,15 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.49.0.tgz", - "integrity": "sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==", + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.53.1.tgz", + "integrity": "sha512-WYC4FB5Ra0xidsmlPb+1SsnaSKPmS3gsjIARwbEkHkoWloQmuzcfypljaJcR78uyLA1h8sHdWWPHSLDI+MtNog==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.49.0", - "@typescript-eslint/types": "^8.49.0", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.53.1", + "@typescript-eslint/types": "^8.53.1", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1697,14 +1739,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.49.0.tgz", - "integrity": "sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==", + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.53.1.tgz", + "integrity": "sha512-Lu23yw1uJMFY8cUeq7JlrizAgeQvWugNQzJp8C3x8Eo5Jw5Q2ykMdiiTB9vBVOOUBysMzmRRmUfwFrZuI2C4SQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.49.0", - "@typescript-eslint/visitor-keys": "8.49.0" + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1715,9 +1757,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.49.0.tgz", - "integrity": "sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==", + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.53.1.tgz", + "integrity": "sha512-qfvLXS6F6b1y43pnf0pPbXJ+YoXIC7HKg0UGZ27uMIemKMKA6XH2DTxsEDdpdN29D+vHV07x/pnlPNVLhdhWiA==", "dev": true, "license": "MIT", "engines": { @@ -1732,17 +1774,17 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.49.0.tgz", - "integrity": "sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==", + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.53.1.tgz", + "integrity": "sha512-MOrdtNvyhy0rHyv0ENzub1d4wQYKb2NmIqG7qEqPWFW7Mpy2jzFC3pQ2yKDvirZB7jypm5uGjF2Qqs6OIqu47w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.49.0", - "@typescript-eslint/typescript-estree": "8.49.0", - "@typescript-eslint/utils": "8.49.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1", + "@typescript-eslint/utils": "8.53.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1757,9 +1799,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.49.0.tgz", - "integrity": "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==", + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.53.1.tgz", + "integrity": "sha512-jr/swrr2aRmUAUjW5/zQHbMaui//vQlsZcJKijZf3M26bnmLj8LyZUpj8/Rd6uzaek06OWsqdofN/Thenm5O8A==", "dev": true, "license": "MIT", "engines": { @@ -1771,21 +1813,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.49.0.tgz", - "integrity": "sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==", + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.53.1.tgz", + "integrity": "sha512-RGlVipGhQAG4GxV1s34O91cxQ/vWiHJTDHbXRr0li2q/BGg3RR/7NM8QDWgkEgrwQYCvmJV9ichIwyoKCQ+DTg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.49.0", - "@typescript-eslint/tsconfig-utils": "8.49.0", - "@typescript-eslint/types": "8.49.0", - "@typescript-eslint/visitor-keys": "8.49.0", - "debug": "^4.3.4", - "minimatch": "^9.0.4", - "semver": "^7.6.0", + "@typescript-eslint/project-service": "8.53.1", + "@typescript-eslint/tsconfig-utils": "8.53.1", + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/visitor-keys": "8.53.1", + "debug": "^4.4.3", + "minimatch": "^9.0.5", + "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1799,16 +1841,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.49.0.tgz", - "integrity": "sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==", + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.53.1.tgz", + "integrity": "sha512-c4bMvGVWW4hv6JmDUEG7fSYlWOl3II2I4ylt0NM+seinYQlZMQIaKaXIIVJWt9Ofh6whrpM+EdDQXKXjNovvrg==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.49.0", - "@typescript-eslint/types": "8.49.0", - "@typescript-eslint/typescript-estree": "8.49.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.53.1", + "@typescript-eslint/types": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1823,13 +1865,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.49.0.tgz", - "integrity": "sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==", + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.53.1.tgz", + "integrity": "sha512-oy+wV7xDKFPRyNggmXuZQSBzvoLnpmJs+GhzRhPjrxl2b/jIlyjVokzm47CZCDUdXKr2zd7ZLodPfOBpOPyPlg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/types": "8.53.1", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -1941,9 +1983,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.5", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.5.tgz", - "integrity": "sha512-D5vIoztZOq1XM54LUdttJVc96ggEsIfju2JBvht06pSzpckp3C7HReun67Bghzrtdsq9XdMGbSSB3v3GhMNmAA==", + "version": "2.9.17", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.17.tgz", + "integrity": "sha512-agD0MgJFUP/4nvjqzIB29zRPUuCF7Ge6mEv9s8dHrtYD7QWXRcx75rOADE/d5ah1NI+0vkDl0yorDd5U852IQQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -2005,9 +2047,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001759", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", - "integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==", + "version": "1.0.30001765", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001765.tgz", + "integrity": "sha512-LWcNtSyZrakjECqmpP4qdg0MMGdN368D7X8XvvAqOcqMv0RxnlqVKZl2V6/mBR68oYMxOZPLw/gO7DuisMHUvQ==", "dev": true, "funding": [ { @@ -2162,16 +2204,16 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.267", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", - "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "version": "1.5.277", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.277.tgz", + "integrity": "sha512-wKXFZw4erWmmOz5N/grBoJ2XrNJGDFMu2+W5ACHza5rHtvsqrK4gb6rnLC7XxKB9WlJ+RmyQatuEXmtm86xbnw==", "dev": true, "license": "ISC" }, "node_modules/esbuild": { - "version": "0.25.12", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", - "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.2.tgz", + "integrity": "sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2182,32 +2224,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.12", - "@esbuild/android-arm": "0.25.12", - "@esbuild/android-arm64": "0.25.12", - "@esbuild/android-x64": "0.25.12", - "@esbuild/darwin-arm64": "0.25.12", - "@esbuild/darwin-x64": "0.25.12", - "@esbuild/freebsd-arm64": "0.25.12", - "@esbuild/freebsd-x64": "0.25.12", - "@esbuild/linux-arm": "0.25.12", - "@esbuild/linux-arm64": "0.25.12", - "@esbuild/linux-ia32": "0.25.12", - "@esbuild/linux-loong64": "0.25.12", - "@esbuild/linux-mips64el": "0.25.12", - "@esbuild/linux-ppc64": "0.25.12", - "@esbuild/linux-riscv64": "0.25.12", - "@esbuild/linux-s390x": "0.25.12", - "@esbuild/linux-x64": "0.25.12", - "@esbuild/netbsd-arm64": "0.25.12", - "@esbuild/netbsd-x64": "0.25.12", - "@esbuild/openbsd-arm64": "0.25.12", - "@esbuild/openbsd-x64": "0.25.12", - "@esbuild/openharmony-arm64": "0.25.12", - "@esbuild/sunos-x64": "0.25.12", - "@esbuild/win32-arm64": "0.25.12", - "@esbuild/win32-ia32": "0.25.12", - "@esbuild/win32-x64": "0.25.12" + "@esbuild/aix-ppc64": "0.27.2", + "@esbuild/android-arm": "0.27.2", + "@esbuild/android-arm64": "0.27.2", + "@esbuild/android-x64": "0.27.2", + "@esbuild/darwin-arm64": "0.27.2", + "@esbuild/darwin-x64": "0.27.2", + "@esbuild/freebsd-arm64": "0.27.2", + "@esbuild/freebsd-x64": "0.27.2", + "@esbuild/linux-arm": "0.27.2", + "@esbuild/linux-arm64": "0.27.2", + "@esbuild/linux-ia32": "0.27.2", + "@esbuild/linux-loong64": "0.27.2", + "@esbuild/linux-mips64el": "0.27.2", + "@esbuild/linux-ppc64": "0.27.2", + "@esbuild/linux-riscv64": "0.27.2", + "@esbuild/linux-s390x": "0.27.2", + "@esbuild/linux-x64": "0.27.2", + "@esbuild/netbsd-arm64": "0.27.2", + "@esbuild/netbsd-x64": "0.27.2", + "@esbuild/openbsd-arm64": "0.27.2", + "@esbuild/openbsd-x64": "0.27.2", + "@esbuild/openharmony-arm64": "0.27.2", + "@esbuild/sunos-x64": "0.27.2", + "@esbuild/win32-arm64": "0.27.2", + "@esbuild/win32-ia32": "0.27.2", + "@esbuild/win32-x64": "0.27.2" } }, "node_modules/escalade": { @@ -2234,9 +2276,9 @@ } }, "node_modules/eslint": { - "version": "9.39.1", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", - "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", + "version": "9.39.2", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.2.tgz", + "integrity": "sha512-LEyamqS7W5HB3ujJyvi0HQK/dtVINZvd5mAAp9eT5S/ujByGjiZLCzPcHVzuXbpJDJF/cxwHlfceVUDZ2lnSTw==", "dev": true, "license": "MIT", "dependencies": { @@ -2246,7 +2288,7 @@ "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.39.1", + "@eslint/js": "9.39.2", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", @@ -2330,9 +2372,9 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz", - "integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==", + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2448,9 +2490,9 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2514,6 +2556,24 @@ "dev": true, "license": "MIT" }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/file-entry-cache": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", @@ -2604,9 +2664,9 @@ } }, "node_modules/globals": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", - "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.0.0.tgz", + "integrity": "sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw==", "dev": true, "license": "MIT", "engines": { @@ -3010,6 +3070,19 @@ "dev": true, "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -3076,30 +3149,30 @@ } }, "node_modules/react": { - "version": "19.2.1", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz", - "integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz", - "integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.1" + "react": "^19.2.3" } }, "node_modules/react-router": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.10.1.tgz", - "integrity": "sha512-gHL89dRa3kwlUYtRQ+m8NmxGI6CgqN+k4XyGjwcFoQwwCWF6xXpOCUlDovkXClS0d0XJN/5q7kc5W3kiFEd0Yw==", + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.12.0.tgz", + "integrity": "sha512-kTPDYPFzDVGIIGNLS5VJykK0HfHLY5MF3b+xj0/tTyNYL1gF1qs7u67Z9jEhQk2sQ98SUaHxlG31g1JtF7IfVw==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -3119,12 +3192,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.10.1.tgz", - "integrity": "sha512-JNBANI6ChGVjA5bwsUIwJk7LHKmqB4JYnYfzFwyp2t12Izva11elds2jx7Yfoup2zssedntwU0oZ5DEmk5Sdaw==", + "version": "7.12.0", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.12.0.tgz", + "integrity": "sha512-pfO9fiBcpEfX4Tx+iTYKDtPbrSLLCbwJ5EqP+SPYQu1VYCXdy79GSj0wttR0U4cikVdlImZuEZ/9ZNCgoaxwBA==", "license": "MIT", "dependencies": { - "react-router": "7.10.1" + "react-router": "7.12.0" }, "engines": { "node": ">=20.0.0" @@ -3159,9 +3232,9 @@ } }, "node_modules/rollup": { - "version": "4.53.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", - "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", + "version": "4.56.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.56.0.tgz", + "integrity": "sha512-9FwVqlgUHzbXtDg9RCMgodF3Ua4Na6Gau+Sdt9vyCN4RhHfVKX2DCHy3BjMLTDd47ITDhYAnTwGulWTblJSDLg==", "dev": true, "license": "MIT", "dependencies": { @@ -3175,28 +3248,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.53.2", - "@rollup/rollup-android-arm64": "4.53.2", - "@rollup/rollup-darwin-arm64": "4.53.2", - "@rollup/rollup-darwin-x64": "4.53.2", - "@rollup/rollup-freebsd-arm64": "4.53.2", - "@rollup/rollup-freebsd-x64": "4.53.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", - "@rollup/rollup-linux-arm-musleabihf": "4.53.2", - "@rollup/rollup-linux-arm64-gnu": "4.53.2", - "@rollup/rollup-linux-arm64-musl": "4.53.2", - "@rollup/rollup-linux-loong64-gnu": "4.53.2", - "@rollup/rollup-linux-ppc64-gnu": "4.53.2", - "@rollup/rollup-linux-riscv64-gnu": "4.53.2", - "@rollup/rollup-linux-riscv64-musl": "4.53.2", - "@rollup/rollup-linux-s390x-gnu": "4.53.2", - "@rollup/rollup-linux-x64-gnu": "4.53.2", - "@rollup/rollup-linux-x64-musl": "4.53.2", - "@rollup/rollup-openharmony-arm64": "4.53.2", - "@rollup/rollup-win32-arm64-msvc": "4.53.2", - "@rollup/rollup-win32-ia32-msvc": "4.53.2", - "@rollup/rollup-win32-x64-gnu": "4.53.2", - "@rollup/rollup-win32-x64-msvc": "4.53.2", + "@rollup/rollup-android-arm-eabi": "4.56.0", + "@rollup/rollup-android-arm64": "4.56.0", + "@rollup/rollup-darwin-arm64": "4.56.0", + "@rollup/rollup-darwin-x64": "4.56.0", + "@rollup/rollup-freebsd-arm64": "4.56.0", + "@rollup/rollup-freebsd-x64": "4.56.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.56.0", + "@rollup/rollup-linux-arm-musleabihf": "4.56.0", + "@rollup/rollup-linux-arm64-gnu": "4.56.0", + "@rollup/rollup-linux-arm64-musl": "4.56.0", + "@rollup/rollup-linux-loong64-gnu": "4.56.0", + "@rollup/rollup-linux-loong64-musl": "4.56.0", + "@rollup/rollup-linux-ppc64-gnu": "4.56.0", + "@rollup/rollup-linux-ppc64-musl": "4.56.0", + "@rollup/rollup-linux-riscv64-gnu": "4.56.0", + "@rollup/rollup-linux-riscv64-musl": "4.56.0", + "@rollup/rollup-linux-s390x-gnu": "4.56.0", + "@rollup/rollup-linux-x64-gnu": "4.56.0", + "@rollup/rollup-linux-x64-musl": "4.56.0", + "@rollup/rollup-openbsd-x64": "4.56.0", + "@rollup/rollup-openharmony-arm64": "4.56.0", + "@rollup/rollup-win32-arm64-msvc": "4.56.0", + "@rollup/rollup-win32-ia32-msvc": "4.56.0", + "@rollup/rollup-win32-x64-gnu": "4.56.0", + "@rollup/rollup-win32-x64-msvc": "4.56.0", "fsevents": "~2.3.2" } }, @@ -3308,41 +3384,10 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyglobby/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { @@ -3380,16 +3425,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.49.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.49.0.tgz", - "integrity": "sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==", + "version": "8.53.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.53.1.tgz", + "integrity": "sha512-gB+EVQfP5RDElh9ittfXlhZJdjSU4jUSTyE2+ia8CYyNvet4ElfaLlAIqDvQV9JPknKx0jQH1racTYe/4LaLSg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.49.0", - "@typescript-eslint/parser": "8.49.0", - "@typescript-eslint/typescript-estree": "8.49.0", - "@typescript-eslint/utils": "8.49.0" + "@typescript-eslint/eslint-plugin": "8.53.1", + "@typescript-eslint/parser": "8.53.1", + "@typescript-eslint/typescript-estree": "8.53.1", + "@typescript-eslint/utils": "8.53.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3417,9 +3462,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", - "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "dev": true, "funding": [ { @@ -3458,13 +3503,13 @@ } }, "node_modules/vite": { - "version": "7.2.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz", - "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", + "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.25.0", + "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -3597,50 +3642,6 @@ } } }, - "node_modules/vite-plugin-checker/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/vite/node_modules/fdir": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/vite/node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, "node_modules/vscode-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", @@ -3695,9 +3696,9 @@ } }, "node_modules/zod": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz", - "integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.5.tgz", + "integrity": "sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==", "dev": true, "license": "MIT", "funding": { @@ -3718,9 +3719,9 @@ } }, "node_modules/zustand": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.9.tgz", - "integrity": "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==", + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.10.tgz", + "integrity": "sha512-U1AiltS1O9hSy3rul+Ub82ut2fqIAefiSuwECWt6jlMVUGejvf+5omLcRBSzqbRagSM3hQZbtzdeRc6QVScXTg==", "license": "MIT", "engines": { "node": ">=12.20.0" diff --git a/frontend/package.json b/frontend/package.json index eba43b05..7d827ea6 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,26 +12,26 @@ }, "dependencies": { "dompurify": "^3.3.1", - "react": "^19.2.1", - "react-dom": "^19.2.1", - "react-router-dom": "^7.10.1", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "react-router-dom": "^7.11.0", "zustand": "^5.0.9" }, "devDependencies": { "@types/react": "^19.2.7", "@types/react-dom": "^19.2.3", - "@typescript-eslint/eslint-plugin": "^8.39.0", - "@typescript-eslint/parser": "^8.39.0", + "@typescript-eslint/eslint-plugin": "^8.51.0", + "@typescript-eslint/parser": "^8.51.0", "@vitejs/plugin-react-swc": "^4.2.2", - "eslint": "^9.39.1", + "eslint": "^9.39.2", "eslint-config-prettier": "^10.1.8", "eslint-plugin-react-hooks": "^7.0.1", - "eslint-plugin-react-refresh": "^0.4.24", - "globals": "^16.5.0", + "eslint-plugin-react-refresh": "^0.4.26", + "globals": "^17.0.0", "prettier": "3.7.4", "typescript": "^5.9.3", - "typescript-eslint": "^8.49.0", - "vite": ">=7.2.7", + "typescript-eslint": "^8.51.0", + "vite": ">=7.3.0", "vite-plugin-checker": "^0.12.0" } } diff --git a/frontend/src/api/loader/loadAppsettingsConfig.ts b/frontend/src/api/loader/loadAppsettingsConfig.ts index 19091533..fb05d00c 100644 --- a/frontend/src/api/loader/loadAppsettingsConfig.ts +++ b/frontend/src/api/loader/loadAppsettingsConfig.ts @@ -16,13 +16,13 @@ export type AppSettingsConfigType = { format: string | null; format_sort: string | null; add_metadata: boolean; - add_thumbnail: boolean; subtitle: string | null; subtitle_source: string | null; subtitle_index: boolean; comment_max: string | null; comment_sort: string; cookie_import: boolean; + pot_provider_url: string | null; potoken: boolean; throttledratelimit: number | null; extractor_lang: string | null; diff --git a/frontend/src/pages/ChannelAbout.tsx b/frontend/src/pages/ChannelAbout.tsx index 54898b92..3e966182 100644 --- a/frontend/src/pages/ChannelAbout.tsx +++ b/frontend/src/pages/ChannelAbout.tsx @@ -8,7 +8,6 @@ import Routes from '../configuration/routes/RouteList'; import queueReindex, { ReindexType, ReindexTypeEnum } from '../api/actions/queueReindex'; import formatDate from '../functions/formatDates'; import PaginationDummy from '../components/PaginationDummy'; -import FormattedNumber from '../components/FormattedNumber'; import Button from '../components/Button'; import updateChannelOverwrites from '../api/actions/updateChannelOverwrite'; import useIsAdmin from '../functions/useIsAdmin'; @@ -145,10 +144,6 @@ const ChannelAbout = () => {
- {channel.channel_views > 0 && ( - - )} - {isAdmin && ( <>
diff --git a/frontend/src/pages/Channels.tsx b/frontend/src/pages/Channels.tsx index 2a6fb5f3..e5b7212d 100644 --- a/frontend/src/pages/Channels.tsx +++ b/frontend/src/pages/Channels.tsx @@ -41,7 +41,6 @@ export type ChannelType = { channel_tags?: string[]; channel_thumb_url: string; channel_tvart_url: string; - channel_views: number; }; const Channels = () => { diff --git a/frontend/src/pages/Home.tsx b/frontend/src/pages/Home.tsx index d01a96d4..09ac57f9 100644 --- a/frontend/src/pages/Home.tsx +++ b/frontend/src/pages/Home.tsx @@ -89,7 +89,6 @@ export type DownloadsType = { format: boolean; format_sort: boolean; add_metadata: boolean; - add_thumbnail: boolean; subtitle: boolean; subtitle_source: boolean; subtitle_index: boolean; diff --git a/frontend/src/pages/SettingsApplication.tsx b/frontend/src/pages/SettingsApplication.tsx index 755e0eaa..2d2153a0 100644 --- a/frontend/src/pages/SettingsApplication.tsx +++ b/frontend/src/pages/SettingsApplication.tsx @@ -58,7 +58,6 @@ const SettingsApplication = () => { const [downloadsFormatSort, setDownloadsFormatSort] = useState(null); const [downloadsExtractorLang, setDownloadsExtractorLang] = useState(null); const [embedMetadata, setEmbedMetadata] = useState(false); - const [embedThumbnail, setEmbedThumbnail] = useState(false); // Subtitles const [subtitleLang, setSubtitleLang] = useState(null); @@ -74,6 +73,7 @@ const SettingsApplication = () => { const [showCookieForm, setShowCookieForm] = useState(false); const [poTokenFormData, setPoTokenFormData] = useState('web+'); const [showPoTokenForm, setShowPoTokenForm] = useState(false); + const [potProviderUrl, setPotProviderUrl] = useState(null); // Integrations const [showApiToken, setShowApiToken] = useState(false); @@ -116,7 +116,6 @@ const SettingsApplication = () => { setDownloadsFormatSort(appSettingsConfigData?.downloads.format_sort || null); setDownloadsExtractorLang(appSettingsConfigData?.downloads.extractor_lang || null); setEmbedMetadata(appSettingsConfigData?.downloads.add_metadata || false); - setEmbedThumbnail(appSettingsConfigData?.downloads.add_thumbnail || false); // Subtitles setSubtitleLang(appSettingsConfigData?.downloads.subtitle || null); @@ -127,6 +126,9 @@ const SettingsApplication = () => { setCommentsMax(appSettingsConfigData?.downloads.comment_max || null); setCommentsSort(appSettingsConfigData?.downloads.comment_sort || ''); + // Cookie + setPotProviderUrl(appSettingsConfigData?.downloads.pot_provider_url || null); + // Integrations setDownloadDislikes(appSettingsConfigData?.downloads.integrate_ryd || false); setEnableSponsorBlock(appSettingsConfigData?.downloads.integrate_sponsorblock || false); @@ -476,9 +478,8 @@ const SettingsApplication = () => { -
  • Embedding metadata adds additional metadata directly to the mp4 file.
  • - Embedding the thumbnail embeds the video thumbnail as a cover.jpg to the mp4 + Embedding metadata adds additional metadata and thumbnails directly to the mp4 file.
  • @@ -533,16 +534,6 @@ const SettingsApplication = () => { updateCallback={handleUpdateConfig} />
    -
    -
    -

    Embed Thumbnail

    -
    - -

    Subtitles

    @@ -714,6 +705,16 @@ const SettingsApplication = () => { PO guide +
  • + The PO Token Provider URL running external to tubearchivist. Make sure to + review{' '} + + User Guide + +
  • )} @@ -813,6 +814,19 @@ const SettingsApplication = () => { )}
    +
    +
    +

    PO Token Provider URL

    +
    + +

    Integrations

    diff --git a/frontend/src/stores/AppSettingsStore.ts b/frontend/src/stores/AppSettingsStore.ts index 844285e5..9db60607 100644 --- a/frontend/src/stores/AppSettingsStore.ts +++ b/frontend/src/stores/AppSettingsStore.ts @@ -23,13 +23,13 @@ export const useAppSettingsStore = create(set => ({ format: null, format_sort: null, add_metadata: false, - add_thumbnail: false, subtitle: null, subtitle_source: null, subtitle_index: false, comment_max: null, comment_sort: 'asc', cookie_import: false, + pot_provider_url: null, potoken: false, throttledratelimit: null, extractor_lang: null, diff --git a/requirements-dev.txt b/requirements-dev.txt index 625e19b1..b944b1d3 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,10 +1,10 @@ -r backend/requirements.txt -ipython==9.8.0 -pre-commit==4.5.0 -pylint-django==2.6.1 -pylint==3.3.9 +ipython==9.9.0 +pre-commit==4.5.1 +pylint-django==2.7.0 +pylint==4.0.4 pytest-django==4.11.1 pytest==9.0.2 python-dotenv==1.2.1 requirementscheck==0.1.0 -types-requests==2.32.4.20250913 +types-requests==2.32.4.20260107