diff --git a/.gitattributes b/.gitattributes index 7bee716a5d..7a5df63373 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,26 +1,6 @@ # Auto-detect text files and perform LF normalization * text=auto -# Fork-specific files - always keep ours during merge conflicts -# These files contain customizations that should not be overwritten by upstream merges -README.md merge=ours -package_info merge=ours -bazarr/app/check_update.py merge=ours -custom_libs/subliminal_patch/providers/opensubtitles_scraper.py merge=ours -Dockerfile merge=ours -docker-compose.yml merge=ours -docker/entrypoint.sh merge=ours -.dockerignore merge=ours -.gitattributes merge=ours -docs/FORK_MAINTENANCE.md merge=ours - -# GitHub workflows - keep our fork's versions -.github/workflows/* merge=ours - -# Git submodule - keep our reference -.gitmodules merge=ours -opensubtitles-scraper merge=ours - # Binary files *.png binary *.jpg binary diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index b663249502..b774a8b9e7 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -21,7 +21,7 @@ A clear and concise description of what you expected to happen. If applicable, add screenshots to help explain your problem. **Software (please complete the following information):** - - Bazarr: [e.g. v 0.6.1] + - Bazarr+: [e.g. v2.0.0] - Radarr version [e.g. v 0.2.0.0001] - Sonarr version [e.g. v 2.0.0.0001] - OS: [e.g. Windows 10] diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 6ceaed40d1..cb76eff4d6 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,11 +1,8 @@ blank_issues_enabled: false contact_links: - name: ๐Ÿ“— Wiki - url: https://github.com/morpheus65535/bazarr/wiki + url: https://wiki.bazarr.media about: The Bazarr wiki should help guide you through installation and setup as well as help resolve common problems and answer frequently asked questions. - name: ๐Ÿš€ Feature suggestions - url: https://bazarr.featureupvote.com/ - about: Share your suggestions or ideas to make Bazarr better! - - name: ๐ŸŒ Discord Support - url: https://discord.gg/MH2e2eb - about: Ask questions and talk about bazarr + url: https://github.com/LavX/bazarr/issues + about: Share your suggestions or ideas to make Bazarr+ better! diff --git a/.github/scripts/build_test.sh b/.github/scripts/build_test.sh index 56ccd6c1a7..d0714b5db0 100755 --- a/.github/scripts/build_test.sh +++ b/.github/scripts/build_test.sh @@ -7,7 +7,7 @@ sleep 30 if kill -s 0 $PID then - echo "Bazarr is still running. We'll test if UI is working..." + echo "Bazarr+ is still running. We'll test if UI is working..." else exit 1 fi diff --git a/.github/workflows/build-docker-manual.yml b/.github/workflows/build-docker-manual.yml index ea7409caa2..e310afe027 100644 --- a/.github/workflows/build-docker-manual.yml +++ b/.github/workflows/build-docker-manual.yml @@ -15,10 +15,6 @@ on: options: - master - development - - feature/audio-display-with-filter - - python_3_14 - - ai_translate - - no_telemetry default: 'master' build_type: @@ -33,7 +29,7 @@ on: default: 'dev' version_tag: - description: 'Version tag (e.g., v1.5.3) - used for release builds' + description: 'Version tag (e.g., v2.0.0) - used for release builds' required: false default: '' type: string @@ -60,82 +56,11 @@ on: - linux/arm64 default: 'linux/amd64,linux/arm64' - skip_scraper: - description: 'Skip building scraper image' - required: false - type: boolean - default: false - env: REGISTRY: ghcr.io - # Note: IMAGE_NAME and SCRAPER_IMAGE_NAME are set dynamically in jobs to ensure lowercase UI_DIRECTORY: ./frontend jobs: - # ========================================================================== - # Build Scraper Image (optional) - # ========================================================================== - build-scraper: - if: ${{ !inputs.skip_scraper }} - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - name: Set lowercase image names - run: | - echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV - echo "SCRAPER_IMAGE_NAME=${GITHUB_REPOSITORY_OWNER,,}/opensubtitles-scraper" >> $GITHUB_ENV - - - name: Checkout - uses: actions/checkout@v5 - with: - ref: ${{ inputs.branch }} - submodules: recursive - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Determine Scraper Tags - id: scraper-tags - run: | - TAGS="${{ env.REGISTRY }}/${{ env.SCRAPER_IMAGE_NAME }}:sha-$(git rev-parse --short HEAD)" - - case "${{ inputs.build_type }}" in - release) - TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.SCRAPER_IMAGE_NAME }}:latest" - ;; - nightly) - TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.SCRAPER_IMAGE_NAME }}:nightly" - ;; - dev) - TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.SCRAPER_IMAGE_NAME }}:dev" - ;; - esac - - echo "tags=$TAGS" >> $GITHUB_OUTPUT - - - name: Build and Push Scraper Image - uses: docker/build-push-action@v6 - with: - context: ./opensubtitles-scraper - file: ./opensubtitles-scraper/Dockerfile - platforms: ${{ inputs.platforms }} - push: true - tags: ${{ steps.scraper-tags.outputs.tags }} - cache-from: type=gha,scope=scraper - cache-to: type=gha,mode=max,scope=scraper - # ========================================================================== # Build Frontend # ========================================================================== @@ -148,7 +73,7 @@ jobs: ref: ${{ inputs.branch }} - name: Setup NodeJS - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: "${{ env.UI_DIRECTORY }}/.nvmrc" cache: 'npm' @@ -185,7 +110,6 @@ jobs: - name: Set lowercase image names run: | echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV - echo "SCRAPER_IMAGE_NAME=${GITHUB_REPOSITORY_OWNER,,}/opensubtitles-scraper" >> $GITHUB_ENV - name: Checkout uses: actions/checkout@v5 @@ -233,7 +157,7 @@ jobs: VERSION=$(git describe --tags --always 2>/dev/null || echo "0.0.0") fi VERSION="${VERSION#v}" # Remove 'v' prefix - FORK_VERSION="${VERSION}-lavx" + FORK_VERSION="${VERSION}" TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${FORK_VERSION}" TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:v${VERSION}" @@ -277,11 +201,16 @@ jobs: if [ -z "$CUSTOM_TAG" ]; then CUSTOM_TAG="custom-${SHORT_SHA}" fi - FORK_VERSION="${CUSTOM_TAG}" - + # Use version_tag for display if provided, otherwise custom tag + if [ -n "${{ inputs.version_tag }}" ]; then + FORK_VERSION="${{ inputs.version_tag }}" + else + FORK_VERSION="${CUSTOM_TAG}" + fi + TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${CUSTOM_TAG}" TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${SHORT_SHA}" - + echo "display_version=${FORK_VERSION}" >> $GITHUB_OUTPUT ;; esac @@ -322,8 +251,8 @@ jobs: push: true tags: ${{ steps.version.outputs.tags }} labels: | - org.opencontainers.image.title=Bazarr (LavX Fork) - org.opencontainers.image.description=Bazarr with OpenSubtitles.org scraper support + org.opencontainers.image.title=Bazarr+ + org.opencontainers.image.description=Bazarr+ - enhanced subtitle management org.opencontainers.image.vendor=LavX org.opencontainers.image.version=${{ steps.version.outputs.fork_version }} org.opencontainers.image.revision=${{ github.sha }} diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 9f6047b681..4522e7f11a 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -3,7 +3,6 @@ name: Build Docker Image on: push: branches: - - main - master paths-ignore: - '*.md' @@ -12,82 +11,16 @@ on: workflow_dispatch: inputs: version_tag: - description: 'Version tag for the image (e.g., v1.5.3)' - required: false - default: '' - type: string - workflow_call: - inputs: - version_tag: - description: 'Version tag for the image' + description: 'Version tag for the image (e.g., v2.0.0)' required: false default: '' type: string env: REGISTRY: ghcr.io - # Note: IMAGE_NAME and SCRAPER_IMAGE_NAME are set dynamically in jobs to ensure lowercase UI_DIRECTORY: ./frontend jobs: - # ========================================================================== - # Build Scraper Image (separate job with its own cache) - # ========================================================================== - build-scraper: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - steps: - - name: Set lowercase image names - run: | - echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV - echo "SCRAPER_IMAGE_NAME=${GITHUB_REPOSITORY_OWNER,,}/opensubtitles-scraper" >> $GITHUB_ENV - - - name: Checkout - uses: actions/checkout@v5 - with: - submodules: recursive - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract Scraper Metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ env.SCRAPER_IMAGE_NAME }} - tags: | - type=raw,value=latest,enable={{is_default_branch}} - type=sha,prefix=sha- - type=ref,event=branch - labels: | - org.opencontainers.image.title=OpenSubtitles.org Scraper - org.opencontainers.image.description=Web scraper service for OpenSubtitles.org - org.opencontainers.image.vendor=LavX - - - name: Build and Push Scraper Image - uses: docker/build-push-action@v6 - with: - context: ./opensubtitles-scraper - file: ./opensubtitles-scraper/Dockerfile - platforms: linux/amd64,linux/arm64 - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha,scope=scraper - cache-to: type=gha,mode=max,scope=scraper - # ========================================================================== # Build Frontend (cached via GitHub Actions cache) # ========================================================================== @@ -98,7 +31,7 @@ jobs: uses: actions/checkout@v5 - name: Setup NodeJS - uses: actions/setup-node@v4 + uses: actions/setup-node@v6 with: node-version-file: "${{ env.UI_DIRECTORY }}/.nvmrc" cache: 'npm' @@ -123,7 +56,7 @@ jobs: # Build and Push Bazarr Image # ========================================================================== build-and-push: - needs: [build-frontend, build-scraper] + needs: [build-frontend] runs-on: ubuntu-latest permissions: contents: read @@ -132,10 +65,9 @@ jobs: id-token: write steps: - - name: Set lowercase image names + - name: Set lowercase image name run: | echo "IMAGE_NAME=${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV - echo "SCRAPER_IMAGE_NAME=${GITHUB_REPOSITORY_OWNER,,}/opensubtitles-scraper" >> $GITHUB_ENV - name: Checkout uses: actions/checkout@v5 @@ -176,19 +108,17 @@ jobs: # Remove 'v' prefix for semver SEMVER="${VERSION#v}" - # Create LavX fork version + # Bazarr+ version is the semver tag directly SHORT_SHA=$(git rev-parse --short HEAD) - BUILD_DATE=$(date -u +%Y%m%d) - FORK_VERSION="${SEMVER}-lavx.${BUILD_DATE}" - + FORK_VERSION="${SEMVER}" + echo "version=$VERSION" >> $GITHUB_OUTPUT echo "semver=$SEMVER" >> $GITHUB_OUTPUT echo "fork_version=$FORK_VERSION" >> $GITHUB_OUTPUT echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT - + echo "## Version Info" >> $GITHUB_STEP_SUMMARY - echo "- **Base Version:** $VERSION" >> $GITHUB_STEP_SUMMARY - echo "- **Fork Version:** $FORK_VERSION" >> $GITHUB_STEP_SUMMARY + echo "- **Version:** $FORK_VERSION" >> $GITHUB_STEP_SUMMARY echo "- **Short SHA:** $SHORT_SHA" >> $GITHUB_STEP_SUMMARY - name: Extract Docker Metadata @@ -199,15 +129,15 @@ jobs: tags: | # Latest tag on main/master branch type=raw,value=latest,enable={{is_default_branch}} - # Version tag (e.g., v1.5.3-lavx.20241214) + # Version tag (e.g., v2.0.0) type=raw,value=${{ steps.version.outputs.fork_version }} # Short SHA tag type=raw,value=sha-${{ steps.version.outputs.short_sha }} # Branch name type=ref,event=branch labels: | - org.opencontainers.image.title=Bazarr (LavX Fork) - org.opencontainers.image.description=Bazarr with OpenSubtitles.org scraper support + org.opencontainers.image.title=Bazarr+ + org.opencontainers.image.description=Bazarr+ - enhanced subtitle management org.opencontainers.image.vendor=LavX org.opencontainers.image.version=${{ steps.version.outputs.fork_version }} @@ -242,17 +172,12 @@ jobs: run: | echo "## ๐Ÿณ Docker Images Published" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "### Bazarr (LavX Fork)" >> $GITHUB_STEP_SUMMARY + echo "### Bazarr+" >> $GITHUB_STEP_SUMMARY echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY echo "docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" >> $GITHUB_STEP_SUMMARY echo "docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.fork_version }}" >> $GITHUB_STEP_SUMMARY echo "\`\`\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "### OpenSubtitles Scraper" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY - echo "docker pull ${{ env.REGISTRY }}/${{ env.SCRAPER_IMAGE_NAME }}:latest" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY echo "### Image Details" >> $GITHUB_STEP_SUMMARY echo "- **Digest:** \`${{ steps.push.outputs.digest }}\`" >> $GITHUB_STEP_SUMMARY echo "- **Platforms:** linux/amd64, linux/arm64" >> $GITHUB_STEP_SUMMARY @@ -279,17 +204,12 @@ jobs: ## ๐Ÿณ Docker Images ```bash - # Bazarr (LavX Fork) docker pull ghcr.io/${{ github.repository }}:${{ github.ref_name }} - - # OpenSubtitles Scraper - docker pull ghcr.io/${{ github.repository_owner }}/opensubtitles-scraper:latest ``` ## ๐Ÿ“ Changes This release is based on upstream Bazarr with the following custom modifications: - OpenSubtitles.org web scraper provider (no VIP API needed) - - Auto-sync with upstream daily at 4 AM UTC See the auto-generated release notes below for detailed changes. \ No newline at end of file diff --git a/.github/workflows/build-scraper.yml b/.github/workflows/build-scraper.yml deleted file mode 100644 index c0556a4b34..0000000000 --- a/.github/workflows/build-scraper.yml +++ /dev/null @@ -1,276 +0,0 @@ -name: Build OpenSubtitles Scraper - -# ============================================================================= -# Standalone workflow for building the OpenSubtitles Scraper Docker image -# Can be triggered manually with version and platform options -# ============================================================================= - -on: - workflow_dispatch: - inputs: - build_type: - description: 'Build type' - required: true - type: choice - options: - - release # latest + version tag - - dev # dev + sha tag - - nightly # nightly + date tag - default: 'dev' - - version_tag: - description: 'Version tag (e.g., v1.0.0) - used for release builds' - required: false - default: '' - type: string - - python_version: - description: 'Python version for the image' - required: true - type: choice - options: - - '3.14' - - '3.13' - - '3.12' - default: '3.14' - - enable_jit: - description: 'Enable Python JIT compiler' - required: false - type: boolean - default: true - - platforms: - description: 'Target platforms' - required: true - type: choice - options: - - linux/amd64,linux/arm64 - - linux/amd64 - - linux/arm64 - default: 'linux/amd64,linux/arm64' - - push_latest: - description: 'Also tag as latest' - required: false - type: boolean - default: false - -env: - REGISTRY: ghcr.io - # Note: IMAGE_NAME is set dynamically in job to ensure lowercase - -jobs: - build-scraper: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - attestations: write - id-token: write - - steps: - - name: Set lowercase image name - run: | - echo "IMAGE_NAME=${GITHUB_REPOSITORY_OWNER,,}/opensubtitles-scraper" >> $GITHUB_ENV - - - name: Checkout - uses: actions/checkout@v5 - with: - submodules: recursive - fetch-depth: 0 - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Determine Version and Tags - id: version - working-directory: ./opensubtitles-scraper - run: | - SHORT_SHA=$(git rev-parse --short HEAD) - BUILD_DATE=$(date -u +%Y%m%d) - PYTHON_VER="${{ inputs.python_version }}" - JIT_SUFFIX="${{ inputs.enable_jit == true && '-jit' || '' }}" - - # Initialize tags - TAGS="" - VERSION="" - - case "${{ inputs.build_type }}" in - release) - if [ -n "${{ inputs.version_tag }}" ]; then - VERSION="${{ inputs.version_tag }}" - else - VERSION=$(git describe --tags --always 2>/dev/null || echo "1.0.0") - fi - VERSION="${VERSION#v}" - DISPLAY_VERSION="v${VERSION}-py${PYTHON_VER}${JIT_SUFFIX}" - - TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${VERSION}" - TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:v${VERSION}" - TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:py${PYTHON_VER}${JIT_SUFFIX}" - ;; - - nightly) - VERSION="nightly-${BUILD_DATE}" - DISPLAY_VERSION="${VERSION}-py${PYTHON_VER}${JIT_SUFFIX}" - - TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:nightly" - TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:nightly-${BUILD_DATE}" - TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:nightly-py${PYTHON_VER}${JIT_SUFFIX}" - ;; - - dev) - VERSION="dev-${SHORT_SHA}" - DISPLAY_VERSION="${VERSION}-py${PYTHON_VER}${JIT_SUFFIX}" - - TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev" - TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev-${SHORT_SHA}" - TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev-py${PYTHON_VER}${JIT_SUFFIX}" - ;; - esac - - # Always add sha tag for immutability - TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${SHORT_SHA}" - - # Add latest if requested - if [ "${{ inputs.push_latest }}" == "true" ]; then - TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" - fi - - echo "tags=${TAGS}" >> $GITHUB_OUTPUT - echo "version=${VERSION}" >> $GITHUB_OUTPUT - echo "display_version=${DISPLAY_VERSION}" >> $GITHUB_OUTPUT - echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT - echo "python_version=${PYTHON_VER}" >> $GITHUB_OUTPUT - echo "jit_enabled=${{ inputs.enable_jit }}" >> $GITHUB_OUTPUT - - # Build summary - echo "## ๐Ÿท๏ธ Scraper Build Info" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY - echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| **Build Type** | ${{ inputs.build_type }} |" >> $GITHUB_STEP_SUMMARY - echo "| **Version** | ${DISPLAY_VERSION} |" >> $GITHUB_STEP_SUMMARY - echo "| **Python** | ${PYTHON_VER} |" >> $GITHUB_STEP_SUMMARY - echo "| **JIT** | ${{ inputs.enable_jit }} |" >> $GITHUB_STEP_SUMMARY - echo "| **Platforms** | ${{ inputs.platforms }} |" >> $GITHUB_STEP_SUMMARY - echo "| **SHA** | ${SHORT_SHA} |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Tags" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "${TAGS}" | tr ',' '\n' >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - - - name: Generate Dockerfile with Python version - run: | - cd opensubtitles-scraper - - # Create a modified Dockerfile with the selected Python version - cat > Dockerfile.build << EOF - # Generated Dockerfile with Python ${{ steps.version.outputs.python_version }} - FROM python:${{ steps.version.outputs.python_version }}-slim - - # Set working directory - WORKDIR /app - - # Install system dependencies - RUN apt-get update && apt-get install -y \\ - gcc \\ - && rm -rf /var/lib/apt/lists/* - - # Set environment variables - ENV PYTHONDONTWRITEBYTECODE=1 \\ - PYTHONUNBUFFERED=1 - EOF - - # Add JIT env var if enabled - if [ "${{ inputs.enable_jit }}" == "true" ]; then - echo 'ENV PYTHON_JIT=1' >> Dockerfile.build - fi - - cat >> Dockerfile.build << 'EOF' - - # Copy requirements file - COPY requirements.txt . - - # Install Python dependencies - RUN pip install --no-cache-dir --upgrade pip && \ - pip install --no-cache-dir -r requirements.txt - - # Copy application code - COPY . . - - # Expose port 8000 - EXPOSE 8000 - - # Health check - HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1 - - # Run the application - CMD ["python", "main.py"] - EOF - - cat Dockerfile.build - - - name: Build and Push Scraper Image - id: push - uses: docker/build-push-action@v6 - with: - context: ./opensubtitles-scraper - file: ./opensubtitles-scraper/Dockerfile.build - platforms: ${{ inputs.platforms }} - push: true - tags: ${{ steps.version.outputs.tags }} - labels: | - org.opencontainers.image.title=OpenSubtitles.org Scraper - org.opencontainers.image.description=Web scraper service for OpenSubtitles.org (LavX Fork) - org.opencontainers.image.vendor=LavX - org.opencontainers.image.version=${{ steps.version.outputs.display_version }} - org.opencontainers.image.revision=${{ github.sha }} - org.opencontainers.image.source=https://github.com/${{ github.repository }} - org.opencontainers.image.python-version=${{ steps.version.outputs.python_version }} - org.opencontainers.image.jit-enabled=${{ inputs.enable_jit }} - cache-from: type=gha,scope=scraper-${{ inputs.python_version }} - cache-to: type=gha,mode=max,scope=scraper-${{ inputs.python_version }} - - - name: Generate Artifact Attestation - uses: actions/attest-build-provenance@v2 - with: - subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - subject-digest: ${{ steps.push.outputs.digest }} - push-to-registry: true - - - name: Create Build Summary - run: | - echo "" >> $GITHUB_STEP_SUMMARY - echo "## ๐Ÿณ Scraper Image Published" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Pull Commands" >> $GITHUB_STEP_SUMMARY - echo '```bash' >> $GITHUB_STEP_SUMMARY - echo "# By version" >> $GITHUB_STEP_SUMMARY - echo "docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "# By SHA (immutable)" >> $GITHUB_STEP_SUMMARY - echo "docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ steps.version.outputs.short_sha }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "# With Python version tag" >> $GITHUB_STEP_SUMMARY - echo "docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ inputs.build_type }}-py${{ inputs.python_version }}${{ inputs.enable_jit == true && '-jit' || '' }}" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Image Details" >> $GITHUB_STEP_SUMMARY - echo "- **Digest:** \`${{ steps.push.outputs.digest }}\`" >> $GITHUB_STEP_SUMMARY - echo "- **Platforms:** ${{ inputs.platforms }}" >> $GITHUB_STEP_SUMMARY - echo "- **Python Version:** ${{ inputs.python_version }}" >> $GITHUB_STEP_SUMMARY - echo "- **JIT Enabled:** ${{ inputs.enable_jit }}" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/build-translator.yml b/.github/workflows/build-translator.yml deleted file mode 100644 index 1d1fa34fab..0000000000 --- a/.github/workflows/build-translator.yml +++ /dev/null @@ -1,306 +0,0 @@ -name: Build AI Subtitle Translator - -# ============================================================================= -# Standalone workflow for building the AI Subtitle Translator Docker image -# Can be triggered manually with version and platform options -# ============================================================================= - -on: - workflow_dispatch: - inputs: - source_branch: - description: 'Branch containing the ai-subtitle-translator submodule' - required: true - type: choice - options: - - ai_translate - - development - - master - default: 'ai_translate' - - build_type: - description: 'Build type' - required: true - type: choice - options: - - release # latest + version tag - - dev # dev + sha tag - - nightly # nightly + date tag - default: 'dev' - - version_tag: - description: 'Version tag (e.g., v1.0.0) - used for release builds' - required: false - default: '' - type: string - - python_version: - description: 'Python version for the image' - required: true - type: choice - options: - - '3.14' - - '3.13' - - '3.12' - default: '3.14' - - enable_jit: - description: 'Enable Python JIT compiler' - required: false - type: boolean - default: true - - platforms: - description: 'Target platforms' - required: true - type: choice - options: - - linux/amd64,linux/arm64 - - linux/amd64 - - linux/arm64 - default: 'linux/amd64,linux/arm64' - - push_latest: - description: 'Also tag as latest' - required: false - type: boolean - default: false - -env: - REGISTRY: ghcr.io - # Force lowercase for Docker image names - IMAGE_NAME: lavx/ai-subtitle-translator - -jobs: - build-translator: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - attestations: write - id-token: write - - steps: - - name: Checkout - uses: actions/checkout@v5 - with: - ref: ${{ inputs.source_branch }} - submodules: recursive - fetch-depth: 0 - - - name: Verify submodule exists - run: | - if [ ! -d "ai-subtitle-translator" ]; then - echo "โŒ Error: ai-subtitle-translator submodule not found!" - echo "The selected branch '${{ inputs.source_branch }}' may not have the submodule configured." - echo "" - echo "Available branches with the submodule:" - echo " - ai_translate (recommended)" - exit 1 - fi - echo "โœ… Submodule found: ai-subtitle-translator" - ls -la ai-subtitle-translator/ - - - name: Set up QEMU - uses: docker/setup-qemu-action@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Determine Version and Tags - id: version - working-directory: ./ai-subtitle-translator - run: | - SHORT_SHA=$(git rev-parse --short HEAD) - BUILD_DATE=$(date -u +%Y%m%d) - PYTHON_VER="${{ inputs.python_version }}" - JIT_SUFFIX="${{ inputs.enable_jit == true && '-jit' || '' }}" - - # Initialize tags - TAGS="" - VERSION="" - - case "${{ inputs.build_type }}" in - release) - if [ -n "${{ inputs.version_tag }}" ]; then - VERSION="${{ inputs.version_tag }}" - else - VERSION=$(git describe --tags --always 2>/dev/null || echo "1.0.0") - fi - VERSION="${VERSION#v}" - DISPLAY_VERSION="v${VERSION}-py${PYTHON_VER}${JIT_SUFFIX}" - - TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${VERSION}" - TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:v${VERSION}" - TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:py${PYTHON_VER}${JIT_SUFFIX}" - ;; - - nightly) - VERSION="nightly-${BUILD_DATE}" - DISPLAY_VERSION="${VERSION}-py${PYTHON_VER}${JIT_SUFFIX}" - - TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:nightly" - TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:nightly-${BUILD_DATE}" - TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:nightly-py${PYTHON_VER}${JIT_SUFFIX}" - ;; - - dev) - VERSION="dev-${SHORT_SHA}" - DISPLAY_VERSION="${VERSION}-py${PYTHON_VER}${JIT_SUFFIX}" - - TAGS="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev" - TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev-${SHORT_SHA}" - TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:dev-py${PYTHON_VER}${JIT_SUFFIX}" - ;; - esac - - # Always add sha tag for immutability - TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${SHORT_SHA}" - - # Add latest if requested - if [ "${{ inputs.push_latest }}" == "true" ]; then - TAGS="${TAGS},${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest" - fi - - echo "tags=${TAGS}" >> $GITHUB_OUTPUT - echo "version=${VERSION}" >> $GITHUB_OUTPUT - echo "display_version=${DISPLAY_VERSION}" >> $GITHUB_OUTPUT - echo "short_sha=${SHORT_SHA}" >> $GITHUB_OUTPUT - echo "python_version=${PYTHON_VER}" >> $GITHUB_OUTPUT - echo "jit_enabled=${{ inputs.enable_jit }}" >> $GITHUB_OUTPUT - - # Build summary - echo "## ๐Ÿท๏ธ AI Subtitle Translator Build Info" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY - echo "|----------|-------|" >> $GITHUB_STEP_SUMMARY - echo "| **Build Type** | ${{ inputs.build_type }} |" >> $GITHUB_STEP_SUMMARY - echo "| **Version** | ${DISPLAY_VERSION} |" >> $GITHUB_STEP_SUMMARY - echo "| **Python** | ${PYTHON_VER} |" >> $GITHUB_STEP_SUMMARY - echo "| **JIT** | ${{ inputs.enable_jit }} |" >> $GITHUB_STEP_SUMMARY - echo "| **Platforms** | ${{ inputs.platforms }} |" >> $GITHUB_STEP_SUMMARY - echo "| **SHA** | ${SHORT_SHA} |" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Tags" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "${TAGS}" | tr ',' '\n' >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - - - name: Generate Dockerfile with Python version - run: | - cd ai-subtitle-translator - - # Create a modified Dockerfile with the selected Python version - cat > Dockerfile.build << EOF - # Generated Dockerfile with Python ${{ steps.version.outputs.python_version }} - FROM python:${{ steps.version.outputs.python_version }}-slim - - # Set working directory - WORKDIR /app - - # Install system dependencies - RUN apt-get update && apt-get install -y --no-install-recommends \\ - gcc curl \\ - && rm -rf /var/lib/apt/lists/* - - # Set environment variables - ENV PYTHONDONTWRITEBYTECODE=1 \\ - PYTHONUNBUFFERED=1 - EOF - - # Add JIT env var if enabled - if [ "${{ inputs.enable_jit }}" == "true" ]; then - echo 'ENV PYTHON_JIT=1' >> Dockerfile.build - fi - - cat >> Dockerfile.build << 'EOF' - - # Copy requirements first for better caching - COPY requirements.txt . - - # Install Python dependencies - RUN pip install --no-cache-dir --upgrade pip && \ - pip install --no-cache-dir -r requirements.txt - - # Copy source code and project files - COPY pyproject.toml . - COPY src/ ./src/ - - # Install the package itself (makes subtitle_translator importable) - RUN pip install --no-cache-dir -e . - - # Create non-root user for security - RUN useradd --create-home --shell /bin/bash appuser && \ - chown -R appuser:appuser /app - USER appuser - - # Expose port 8765 - EXPOSE 8765 - - # Health check - HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ - CMD curl -f http://localhost:8765/health || exit 1 - - # Run the application - CMD ["uvicorn", "subtitle_translator.main:app", "--host", "0.0.0.0", "--port", "8765"] - EOF - - cat Dockerfile.build - - - name: Build and Push Translator Image - id: push - uses: docker/build-push-action@v6 - with: - context: ./ai-subtitle-translator - file: ./ai-subtitle-translator/Dockerfile.build - platforms: ${{ inputs.platforms }} - push: true - tags: ${{ steps.version.outputs.tags }} - labels: | - org.opencontainers.image.title=AI Subtitle Translator - org.opencontainers.image.description=LLM-powered subtitle translation service using OpenRouter API - org.opencontainers.image.vendor=LavX - org.opencontainers.image.version=${{ steps.version.outputs.display_version }} - org.opencontainers.image.revision=${{ github.sha }} - org.opencontainers.image.source=https://github.com/${{ github.repository }} - org.opencontainers.image.python-version=${{ steps.version.outputs.python_version }} - org.opencontainers.image.jit-enabled=${{ inputs.enable_jit }} - cache-from: type=gha,scope=translator-${{ inputs.python_version }} - cache-to: type=gha,mode=max,scope=translator-${{ inputs.python_version }} - - - name: Generate Artifact Attestation - uses: actions/attest-build-provenance@v2 - with: - subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - subject-digest: ${{ steps.push.outputs.digest }} - push-to-registry: true - - - name: Create Build Summary - run: | - echo "" >> $GITHUB_STEP_SUMMARY - echo "## ๐Ÿณ AI Subtitle Translator Image Published" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Pull Commands" >> $GITHUB_STEP_SUMMARY - echo '```bash' >> $GITHUB_STEP_SUMMARY - echo "# By version" >> $GITHUB_STEP_SUMMARY - echo "docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "# By SHA (immutable)" >> $GITHUB_STEP_SUMMARY - echo "docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ steps.version.outputs.short_sha }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "# With Python version tag" >> $GITHUB_STEP_SUMMARY - echo "docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ inputs.build_type }}-py${{ inputs.python_version }}${{ inputs.enable_jit == true && '-jit' || '' }}" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Image Details" >> $GITHUB_STEP_SUMMARY - echo "- **Digest:** \`${{ steps.push.outputs.digest }}\`" >> $GITHUB_STEP_SUMMARY - echo "- **Platforms:** ${{ inputs.platforms }}" >> $GITHUB_STEP_SUMMARY - echo "- **Python Version:** ${{ inputs.python_version }}" >> $GITHUB_STEP_SUMMARY - echo "- **JIT Enabled:** ${{ inputs.enable_jit }}" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0848a9e187..6699519db9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [main] + branches: [master, development] paths: - frontend/** - bazarr/** @@ -14,7 +14,7 @@ on: - dev-requirements.txt - .github/workflows/ci.yml pull_request: - branches: [main] + branches: [master, development] env: ROOT_DIRECTORY: . @@ -77,7 +77,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14' ] + python-version: [ '3.12', '3.13', '3.14' ] name: Python ${{ matrix.python-version }} backend steps: @@ -90,7 +90,7 @@ jobs: python-version: ${{ matrix.python-version }} - name: Install UI - uses: actions/download-artifact@v5 + uses: actions/download-artifact@v4 with: name: ${{ env.UI_ARTIFACT_NAME }} path: "${{ env.UI_DIRECTORY }}/build" diff --git a/.github/workflows/test_bazarr_execution.yml b/.github/workflows/test_bazarr_execution.yml index cca016553e..0272c6ebf5 100644 --- a/.github/workflows/test_bazarr_execution.yml +++ b/.github/workflows/test_bazarr_execution.yml @@ -13,7 +13,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: [ '3.8', '3.9', '3.10', '3.11', '3.12', '3.13', '3.14' ] + python-version: [ '3.12', '3.13', '3.14' ] name: Python ${{ matrix.python-version }} test steps: diff --git a/.gitignore b/.gitignore index d70e8a9450..16e1ffd382 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,7 @@ VERSION # Allow !*.dll +.claude/ +.superpowers/ +docs/superpowers/ +.coverage diff --git a/.gitmodules b/.gitmodules index 8bfc782734..08ab19ed6e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "ai-subtitle-translator"] path = ai-subtitle-translator url = https://github.com/LavX/ai-subtitle-translator.git +[submodule "bazarr-binaries"] + path = bazarr-binaries + url = https://github.com/LavX/bazarr-binaries.git diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ccf3419a4d..c9d3609e26 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,45 +1,120 @@ -# How to Contribute +# Contributing to Bazarr+ ## Tools required -- Python 3.8.x to 3.12.x (3.10.x is highly recommended and 3.13 or greater is proscribed). -- Pycharm or Visual Studio code IDE are recommended but if you're happy with VIM, enjoy it! -- Git. -- UI testing must be done using Chrome latest version. +- Python 3.12+ (3.14 recommended, matches Docker image) +- Node.js (version in `frontend/.nvmrc`) +- Git +- Docker and Docker Compose (for integration testing) +- UI testing should be done in Chrome latest version -## Warning +## Branching -As we're using Git in the development process, you better disable automatic update of Bazarr in UI or you may get your changes overwritten. Alternatively, you can completely disable the update module by running Bazarr with `--no-update` command line argument. +### Branch model -## Branching +- `master` contains stable releases, tagged with semver versions (e.g., `v2.0.0`, `v2.1.0`) +- `development` is the integration branch where upstream merges and new features land +- Feature branches are created from `development` and merged back via PR + +### Rules + +- `master` is not merged back to `development` +- All feature branches are branched from `development` +- Cherry-picked upstream fixes go into `development` first, never directly to `master` + +## Upstream relationship + +Bazarr+ is a hard fork of [upstream Bazarr](https://github.com/morpheus65535/bazarr). There is no automatic synchronization. Bug fixes from upstream may be cherry-picked selectively when relevant, but upstream releases are not merged wholesale. + +## Contribution workflow + +1. Fork the repository +2. Create a feature branch from `development` +3. Make your changes +4. Write or update tests for your changes +5. Run linting and tests, verify they pass +6. Submit a PR targeting the `development` branch + +For major changes, open an issue first to discuss the approach. + +## Linting + +All frontend code must pass ESLint before submitting a PR. + +```bash +cd frontend + +# Check for lint errors +npm run check + +# Auto-fix import sorting and formatting +npx eslint --fix --ext .ts,.tsx src/ +``` + +Fix all errors before submitting. Warnings should be addressed when practical. + +## Testing + +PRs should include tests when the change is testable. We use: + +- **Backend:** pytest for Python tests +- **Frontend:** Vitest for React component and page tests + +```bash +# Run backend tests +pytest tests/ + +# Run frontend tests +cd frontend +npm test + +# Run a specific test file +npm test -- Translator +``` + +When to include tests: +- New features: add tests covering the core behavior +- Bug fixes: add a test that reproduces the bug and verifies the fix +- Refactors: ensure existing tests still pass, add tests if coverage gaps exist + +When tests are optional: +- Pure styling/CSS changes +- Documentation updates +- Config file changes + +## Commit messages -### Basic rules +Use conventional commit style: -- `master` contains only stable releases (which have been merged to `master`) and is intended for end-users. -- `development` is the target for testing (around 10% of users) and is not intended for end-users looking for stability. -- `feature` is a temporary feature branch based on `development`. +``` +feat(translator): add batch retry for failed jobs +fix(ui): search field not clearing on page change +refactor(scraper): simplify response parsing +``` -### Conditions +## Submodules -- `master` is not merged back to `development`. -- `development` is not re-based on `master`. -- all `feature` branches are branched from `development` only. -- Bugfixes created specifically for a feature branch are done there (because they are specific, they're not cherry-picked to `development`). -- We will not release a patch (1.0.x) if a newer minor (1.x.0) has already been released. We only go forward. +Bazarr+ includes two submodules: +- `opensubtitles-scraper` - OpenSubtitles.org web scraper service +- `ai-subtitle-translator` - AI-powered subtitle translator -## Typical contribution workflow +Changes to these should be submitted to their respective repositories: +- [LavX/opensubtitles-scraper](https://github.com/LavX/opensubtitles-scraper) +- [LavX/ai-subtitle-translator](https://github.com/LavX/ai-subtitle-translator) -### Community devs +## Running locally -- Fork the repository or pull the latest changes if you already have forked it. -- Checkout `development` branch. -- Make the desired changes. -- Submit a PR to Bazarr `development` branch. -- Once reviewed, your PR will be merged using Squash and Merge with a meaningful commit message matching our standards. +```bash +# Clone with submodules +git clone --recursive https://github.com/LavX/bazarr.git +cd bazarr -### Official devs team +# Backend +pip install -r requirements.txt +python bazarr.py --no-update --config ./config -- All commits must have a meaningful commit message (ex.: Fixed issue with this, Improved process abc, Added input field to UI, etc.). -- Fixes can be made directly to `development` branch but keep in mind that a pre-release with a beta versioning will be created every day a new push is made. -- Features must be developed in dedicated feature branch and merged back to `development` branch using PR. -- Once reviewed, your PR will be merged by morpheus65535 using Squash and Merge with a meaningful message. +# Frontend (separate terminal) +cd frontend +npm ci +npm start +``` diff --git a/Dockerfile b/Dockerfile index 86ddab04ca..7daf41e6ea 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # ============================================================================= -# Bazarr LavX Fork - Production Docker Image +# Bazarr+ Production Docker Image # ============================================================================= # Multi-stage build optimized for layer caching # Based on Debian Slim for better compatibility (unrar, etc.) @@ -42,8 +42,8 @@ ARG BAZARR_VERSION ARG BUILD_DATE ARG VCS_REF -LABEL org.opencontainers.image.title="Bazarr (LavX Fork)" \ - org.opencontainers.image.description="Bazarr with OpenSubtitles.org scraper support" \ +LABEL org.opencontainers.image.title="Bazarr+" \ + org.opencontainers.image.description="Bazarr+ - enhanced subtitle management" \ org.opencontainers.image.version="${BAZARR_VERSION}" \ org.opencontainers.image.created="${BUILD_DATE}" \ org.opencontainers.image.revision="${VCS_REF}" \ @@ -94,7 +94,7 @@ COPY bazarr ./bazarr # Write version to VERSION file so bazarr/main.py can read it RUN echo "${BAZARR_VERSION}" > /app/bazarr/VERSION -# Copy fork identification file (shows "LavX Fork" in System Status) +# Copy package identification file (shows version in System Status) COPY package_info /app/bazarr/package_info # Copy pre-built frontend (built in GitHub Actions workflow for caching) @@ -102,11 +102,9 @@ COPY package_info /app/bazarr/package_info COPY frontend/build ./frontend/build # Set environment variables -# PYTHON_JIT=1 enables the experimental JIT compiler (Python 3.13+) ENV HOME="/config" \ PYTHONDONTWRITEBYTECODE=1 \ - PYTHONUNBUFFERED=1 \ - PYTHON_JIT=1 + PYTHONUNBUFFERED=1 # Volume for persistent data VOLUME /config @@ -116,7 +114,7 @@ EXPOSE 6767 # Health check HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ - CMD curl -f http://localhost:6767/api/system/health || exit 1 + CMD curl -f http://localhost:6767/api/system/ping || exit 1 ENTRYPOINT ["/entrypoint.sh"] CMD ["python", "bazarr.py", "--no-update", "--config", "/config"] \ No newline at end of file diff --git a/README.md b/README.md index bec72fea81..73183295da 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,66 @@ -# Bazarr (LavX Fork) - "Neon Pulse" +

+ Bazarr+ v2.0.0 - Codename: Eclipse +

+ Bazarr+ Logo +

+ +#

Bazarr+ + + Docker Latest Release Docker Build

- Automated subtitle management with OpenSubtitles.org scraper & AI-powered translation + Enhanced subtitle management built on Bazarr

- This fork of Bazarr includes:
- - OpenSubtitles.org provider that works without VIP API credentials (survives the API shutdown)
- - AI Subtitle Translator using OpenRouter LLMs for high-quality subtitle translation
- - Batch translation for entire series/movie libraries
- - Advanced table filters with collapsible panels, active filter pills, and audio language filtering + No tracking ยท Provider priority ยท OpenSubtitles.org web scraper ยท AI translation via OpenRouter (300+ LLMs) ยท API key encryption ยท batch translation ยท mass subtitle sync ยท 11 bulk operations ยท subtitle viewer ยท advanced table filters ยท security hardening ยท Python 3.14 ยท navy + amber dark theme

--- -## OpenSubtitles.org API Shutdown Notice - -On **January 29, 2026**, OpenSubtitles.org [announced the final shutdown](https://forum.opensubtitles.org/viewtopic.php?t=19471) of their legacy XML-RPC API for **all third-party applications** -- both VIP and non-VIP users. The shutdown is taking effect in the coming weeks. +## Switching from upstream Bazarr? -**This fork is not affected.** It includes a self-hosted web scraper that accesses OpenSubtitles.org directly through the website, bypassing the API entirely. As long as the OpenSubtitles.org website remains accessible ([confirmed by the site admin](https://forum.opensubtitles.org/viewtopic.php?t=19471) to stay available as read-only), subtitle search and download will continue to work. +- Migration can be as simple as replacing the container image with `ghcr.io/lavx/bazarr:latest` and starting the container +- Back up your `/config` directory first +- Bazarr+ uses independent versioning starting at v2.0.0, unrelated to upstream version numbers +- Config changes made by Bazarr+ are not backwards-compatible with upstream Bazarr, so switching back requires restoring your backup +- Recommended: test with a copy of your config before committing to the switch -## Why This Fork Exists - -This fork was created after the [pull request (#3012)](https://github.com/morpheus65535/bazarr/pull/3012) to add OpenSubtitles.org web scraper support was declined by the upstream maintainer. The upstream decision is understandable -- they have agreements with OpenSubtitles and want to respect their infrastructure. +--- -However, with the official API now being shut down, this fork with its scraper companion container is one of the few ways to maintain access to OpenSubtitles.org subtitles. The web scraper is rate-limited and respectful to their servers. +## At a Glance + +| Feature | Upstream Bazarr | Bazarr+ | +|---------|-----------------|---------| +| **Provider Priority** | [Rejected](https://bazarr.featureupvote.com/suggestions/112323/provider-prioritization) (62 votes) | Dual mode: priority order with early stop, or classic simultaneous | +| **OpenSubtitles.org (Scraper)** | Not available | Self-hosted FastAPI microservice via CloudScraper | +| **AI Subtitle Translator (OpenRouter)** | Not available | 300+ LLMs + any custom model ID | +| **API Key Encryption** | Not available | AES-256-GCM encryption for keys in transit | +| **Translate from Missing Menu** | Not available | Action menu on missing subs with source language picker | +| **Batch Translation** | Not available | Translate entire series/libraries from Wanted pages | +| **Mass Subtitle Sync** | [Rejected](https://bazarr.featureupvote.com/suggestions/172013/mass-sync-all-subtitles) (249 votes) | Bulk sync from Tasks page or Mass Edit, skips already-synced | +| **Bulk Operations** | One-at-a-time only | 11 batch actions: sync, translate, OCR fixes, common fixes, remove HI, remove tags, fix uppercase, reverse RTL, scan disk, search missing, upgrade (up to 10k items) | +| **Dedicated Translator Settings** | Not available | 4-zone page with pricing, cost estimates, status panel | +| **No Tracking** | GA4 + legacy UA phone home to Google | All telemetry removed, nothing phones home | +| **Security Hardening** | MD5, no CSRF/SSRF/rate limiting | PBKDF2 (600k iter), CSRF, SSRF, brute-force, 4 more | +| **Subtitle Viewer** | Not available | Read-only subtitle preview with SRT/VTT/ASS parsing, cue table, and format detection | +| **Audio Language Display** | Not shown in tables | Badges in all table views | +| **Advanced Table Filters** | No filters | Include/exclude audio, missing subtitle, title search | +| **Floating Save + Ctrl+S** | Not available | Sticky save button with 3-option unsaved changes modal | +| **Navy + Amber Theme** | Purple | `#121125` navy to `#fff8e1` cream, amber accents | +| OpenSubtitles.com (API) | Available | Available | +| Docker images | linuxserver.io / hotio | ghcr.io/lavx (self-built, multi-arch) | +| Python runtime | 3.8-3.13 | 3.14 | --- -## ๐Ÿš€ Quick Start +## Quick Start ### Option 1: Docker Compose (Recommended) @@ -60,66 +86,104 @@ docker pull ghcr.io/lavx/ai-subtitle-translator:latest --- -## ๐Ÿ”Œ What's Different in This Fork? +### Screenshots -| Feature | Upstream Bazarr | LavX Fork | -|---------|-----------------|-----------| -| **OpenSubtitles.org (Scraper)** | โŒ Not available | โœ… Included (API-independent) | -| **AI Subtitle Translator** | โŒ Not available | โœ… Included (OpenRouter, Gemini, Lingarr) | -| **Batch Translation** | โŒ Not available | โœ… Translate entire series/libraries | -| **Audio Language Display** | โŒ Not shown in tables | โœ… Audio languages visible in all table views | -| **Advanced Table Filters** | โŒ Basic search only | โœ… Collapsible filter panel with active filter pills | -| OpenSubtitles.org (API) | Shutting down | N/A (uses scraper instead) | -| OpenSubtitles.com (API) | โœ… Available | โœ… Available | -| Docker images | linuxserver/hotio | ghcr.io/lavx | -| Python runtime | 3.11/3.12 | 3.14 with JIT | -| Fork identification in UI | N/A | โœ… "LavX Fork - Neon Pulse" | +| Series with batch actions | Mass translate dialog | +|:---:|:---:| +| ![Series Batch Actions](screenshot/series-batch-actions.png "Series list with batch toolbar and subtitle tools") | ![Mass Translate](screenshot/mass-translate.png "Mass translate dialog with model and language selection") | -### ๐ŸŽฏ OpenSubtitles.org Scraper Provider +| Series detail with fanart | Subtitle viewer | +|:---:|:---:| +| ![Series Detail](screenshot/series-detail.png "Series detail page with fanart bleed and episode list") | ![Subtitle Viewer](screenshot/subtitle-viewer.png "Read-only subtitle viewer with cue table") | -This fork adds a **new subtitle provider** called "OpenSubtitles.org" that: +| AI Translator settings | +|:---:| +| ![Translator Settings](screenshot/translator-settings-v2.png "AI Translator settings with connection, model tuning, and job queue") | -- โœ… Works **without** API credentials or VIP subscription -- โœ… Searches by IMDB ID for accurate results -- โœ… Supports both movies and TV shows -- โœ… Provides subtitle rating and download count info -- โœ… Runs as a separate microservice for reliability +--- -### ๐Ÿค– AI Subtitle Translator +
+Feature Details -This fork includes an **LLM-powered subtitle translator** that: +### OpenSubtitles.org Web Scraper +OpenSubtitles.org shut down their XML-RPC API for all third-party apps, VIP included. Bazarr+ ships a self-hosted FastAPI microservice that scrapes OpenSubtitles.org directly via CloudScraper with optional FlareSolverr fallback. It provides search, subtitle listing, and download endpoints (`/api/v1/search`, `/api/v1/subtitles`, `/api/v1/download/subtitle`) and integrates into Bazarr's provider system through a mixin class. No API key or VIP subscription needed. -- โœ… Uses **OpenRouter API** for access to 100+ AI models (Gemini, GPT, Claude, LLaMA, Grok, etc.) -- โœ… Translates subtitles when no good match is found in your target language -- โœ… **Async job queue** for handling multiple translations -- โœ… Real-time **progress tracking** in Bazarr UI -- โœ… Configurable directly in Bazarr Settings (API key, model, temperature, concurrent jobs) -- โœ… Runs as a separate microservice for reliability +### Provider Priority +Upstream Bazarr queries all subtitle providers simultaneously and picks the highest-scored result. There's no way to prefer one provider over another. This has been [requested for 6 years](https://bazarr.featureupvote.com/suggestions/112323/provider-prioritization) (62 votes), but upstream rejected it as "won't happen," calling it a "major rework" that "would take months of development." -**Repository:** [github.com/LavX/ai-subtitle-translator](https://github.com/LavX/ai-subtitle-translator) +Bazarr+ solves it with a **Provider Priority toggle** in Settings > Providers. When enabled, providers are queried sequentially in the order you've arranged them. If a provider returns subtitles meeting the minimum score, Bazarr+ stops searching and uses those results. Your preferred providers (curated community sites, specialized language sources) always get first shot. When disabled, the original behavior is preserved: all providers queried simultaneously, best score wins. -### ๐Ÿ” Advanced Table Filters +### AI Subtitle Translation via OpenRouter +Upstream has Google Translate, Gemini, and Lingarr. Bazarr+ adds **OpenRouter** as a fourth translator engine, giving access to 300+ LLMs (Claude, Gemini, GPT, LLaMA, Grok, and more) plus any custom model ID from openrouter.ai. It runs as a separate microservice with an async job queue supporting 1-5 concurrent jobs and 1-8 parallel batches. Features include: +- **Translate from the subtitle action menu**: click (...) on a missing subtitle row, pick an existing source subtitle to translate from +- **Batch translation** for entire series/movie libraries from the Wanted pages +- **Dedicated settings page** with 4 zones: engine picker, connection config, model tuning (temperature, reasoning mode, parallel batches), and a live status panel showing queue stats, job progress, token usage, cost, and speed +- **Model details** fetched live from the OpenRouter API with per-million token pricing, per-episode/movie cost estimates, context length, and prompt caching indicators +- **AES-256-GCM encryption** for API keys in transit between Bazarr and the translator service, with a Test Connection button that validates encryption and API key status before saving +- **Auto disk scan** triggers Sonarr/Radarr to rescan after translation completes -All table views (Movies, Series, Wanted Movies, Wanted Series) feature a **sophisticated filter system**: +### Subtitle Viewer +Read-only subtitle preview accessible from the subtitle action menu. Supports SRT, VTT, and ASS/SSA formats with automatic format detection. Shows a cue table with timestamps and text, file size, and format badge. Useful for quickly checking subtitle content and timing without downloading. -- โœ… **Collapsible filter panel** toggled via a filter button with active filter count badge -- โœ… **Include/Exclude audio language** filters with labeled, searchable multi-select dropdowns -- โœ… **Missing subtitle language** filter (on Wanted pages) -- โœ… **Active filter pills** showing each active filter as a color-coded removable badge -- โœ… **Clear all** button to reset all filters at once -- โœ… **Search by title** with inline clear button +### Advanced UI +- **Table filters** on Wanted and Library pages: include/exclude audio language (multi-select), missing subtitle language filter, title search, with active filter chips and a collapsible filter panel +- **Floating save button** with Ctrl+S/Cmd+S keyboard shortcut, visible only when settings have unsaved changes +- **Three-button unsaved changes modal**: Save & Leave, Discard, or Keep Editing (upstream only has Leave/Stay) +- **Navy + amber dark theme**: custom color palette from `#121125` (navy black) to `#fff8e1` (cream), with amber brand accents (`#e68a00` to `#b36b00`) +- **Audio language display** as blue badges in all table views -| Wanted Movies with filters | Series with audio filter | -|:---:|:---:| -| ![Wanted Movies Filter](/screenshot/filter-wanted-movies.png?raw=true "Wanted Movies with active filters") | ![Series Filter](/screenshot/filter-series-include.png?raw=true "Series with Include Audio filter") | +### Mass Subtitle Sync +Upstream lets you sync subtitles one at a time, or per-series via Mass Edit. But there's no way to sync your entire library at once. This has been [requested for years](https://bazarr.featureupvote.com/suggestions/172013/mass-sync-all-subtitles) (249 votes), but upstream rejected it as "won't happen," saying "Bazarr isn't a batch tool." -| Mass Translate with filtered selection | -|:---:| -| ![Mass Translate](/screenshot/filter-mass-translate.png?raw=true "Mass Translate dialog with filtered items") | +Bazarr+ adds two entry points for bulk sync: +- **System Tasks page**: a "Mass Sync All Subtitles" task with a Run button that syncs every subtitle in your library +- **Mass Edit pages**: a "Sync Subtitles" button for both Movies and Series editors, so you can select specific items and sync their subtitles in bulk ---- +Both use the existing ffsubsync engine. Already-synced subtitles are skipped by default (with a force re-sync option). Configurable max offset, Golden-Section Search, and framerate correction settings. -## ๐Ÿ“ฆ Installation +### Bulk Operations +Select multiple movies or series from the library pages and apply operations in batch. Available from the toolbar that appears when items are selected, or from the System Tasks page for library-wide runs. Confirmation is required for operations on 100+ items. Up to 10,000 items per batch. + +**Subtitle modifications** (applied to all existing subtitles of selected items): +- **Sync** -- align subtitle timing to audio using ffsubsync, with configurable max offset (1-600s), Golden-Section Search, framerate correction, and force re-sync +- **OCR Fixes** -- correct common optical character recognition errors +- **Common Fixes** -- apply standard subtitle formatting and whitespace corrections +- **Remove Hearing Impaired** -- strip `[music]`, `(doorbell rings)`, and similar HI annotations +- **Remove Style Tags** -- remove ``, ``, `` and other formatting tags +- **Fix Uppercase** -- convert ALL CAPS subtitles to proper case +- **Reverse RTL** -- fix right-to-left punctuation for Arabic, Hebrew, and similar languages +- **Translate** -- batch translate subtitles using any configured translator engine (Google, Gemini, Lingarr, or OpenRouter with 300+ LLMs) + +**Media operations** (search and scan actions for selected items): +- **Scan Disk** -- rescan selected items for on-disk subtitle files +- **Search Missing** -- search all configured providers for missing subtitles +- **Upgrade** -- replace low-scoring subtitles with better matches from providers + +**Profile management:** +- **Bulk profile assignment** -- select multiple movies or series and assign a language profile to all of them at once + +### No Tracking / No Telemetry +Upstream Bazarr ships two analytics systems that phone home to Google: a GA4 property (`G-3820T18GE3`) in `bazarr/utilities/analytics.py` that reports your Bazarr version, Python version, Sonarr/Radarr versions, OS, subtitle provider usage, every download action, and languages searched, plus a legacy Universal Analytics tracker (`UA-86466078-1`) in the SubZero library dependency. Bazarr+ has removed both entirely. No usage data leaves your server. + +### Security Hardening +8 areas upstream doesn't address: +- **Password hashing**: PBKDF2-SHA256 with 600,000 iterations and 16-byte random salt (upstream uses plain MD5) +- **CSRF protection**: cryptographic state tokens (`secrets.token_urlsafe(32)`) on Plex OAuth with timing-safe validation +- **SSRF blocking**: DNS pinning with IP validation, blocks loopback and link-local addresses, fails closed +- **Brute-force protection**: 5 failed attempts trigger 300-second lockout per IP, tracks up to 10,000 IPs with thread-safe OrderedDict +- **Shell injection**: replaced naive character escaping with `shlex.quote()` (POSIX) and `subprocess.list2cmdline()` (Windows) +- **Filesystem sandboxing**: blocks `/proc`, `/sys`, `/dev`, `/etc`, `/root`, `/tmp` and 4 others from the filesystem browser, resolves symlinks +- **eval() removal**: replaced `eval()` in throttled provider cache with `json.loads()` to prevent arbitrary code execution +- **API key comparison**: `hmac.compare_digest()` for constant-time comparison (upstream uses Python `in` operator, which is timing-dependent) + +### Python 3.14 +Dockerfile uses `python:3.14-slim-bookworm`. Upstream supports Python 3.8-3.13 and relies on third-party Docker images (LinuxServer.io, hotio). Bazarr+ builds and publishes its own multi-arch image to GHCR. + +
+ + +
+Installation and Configuration ### Docker Compose Setup @@ -127,13 +191,26 @@ Create a `docker-compose.yml` file: ```yaml services: - # OpenSubtitles.org Scraper Service (required for the scraper provider) + # FlareSolverr - Handles browser challenges for web scraping + flaresolverr: + image: ghcr.io/flaresolverr/flaresolverr:latest + container_name: flaresolverr + restart: unless-stopped + ports: + - "8191:8191" + environment: + - LOG_LEVEL=info + opensubtitles-scraper: image: ghcr.io/lavx/opensubtitles-scraper:latest container_name: opensubtitles-scraper restart: unless-stopped + depends_on: + - flaresolverr ports: - "8000:8000" + environment: + - FLARESOLVERR_URL=http://flaresolverr:8191/v1 healthcheck: test: ["CMD", "curl", "-sf", "http://localhost:8000/health"] interval: 30s @@ -150,14 +227,14 @@ services: environment: # OpenRouter API key (can also be configured in Bazarr UI) - OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-} - - OPENROUTER_DEFAULT_MODEL=google/gemini-2.5-flash-preview-05-20 + - OPENROUTER_DEFAULT_MODEL=google/gemini-2.5-flash-lite-preview-09-2025 healthcheck: test: ["CMD", "curl", "-sf", "http://localhost:8765/health"] interval: 30s timeout: 10s retries: 3 - # Bazarr with OpenSubtitles.org scraper support + # Bazarr+ bazarr: image: ghcr.io/lavx/bazarr:latest container_name: bazarr @@ -173,8 +250,6 @@ services: - PUID=1000 - PGID=1000 - TZ=Europe/Budapest - # Enable the web scraper mode (auto-enables "Use Web Scraper" in settings) - - OPENSUBTITLES_USE_WEB_SCRAPER=true # Point to the scraper service (port 8000) - OPENSUBTITLES_SCRAPER_URL=http://opensubtitles-scraper:8000 volumes: @@ -195,32 +270,40 @@ docker compose up -d ### Environment Variables -| Variable | Description | Default | -|----------|-------------|---------| -| `PUID` | User ID for file permissions | `1000` | -| `PGID` | Group ID for file permissions | `1000` | -| `TZ` | Timezone | `UTC` | -| `OPENSUBTITLES_USE_WEB_SCRAPER` | Enable web scraper mode | `false` | -| `OPENSUBTITLES_SCRAPER_URL` | URL of the scraper service | `http://localhost:8000` | +| Variable | Default | Description | +|----------|---------|-------------| +| `PUID` | `1000` | User ID for file permissions | +| `PGID` | `1000` | Group ID for file permissions | +| `TZ` | `UTC` | Timezone (e.g., `Europe/Budapest`) | +| `OPENSUBTITLES_SCRAPER_URL` | `http://opensubtitles-scraper:8000` | OpenSubtitles.org scraper service URL (port 8000, not 8765) | + +### Volumes + +| Path | Description | +|------|-------------| +| `/config` | Bazarr configuration and database | +| `/movies` | Movies library (match your Radarr path) | +| `/tv` | TV shows library (match your Sonarr path) | ### Enabling the Provider -1. Go to **Settings** โ†’ **Providers** -2. Enable **"OpenSubtitles.org"** (not OpenSubtitles.com - that's the API version) -3. If `OPENSUBTITLES_USE_WEB_SCRAPER=true` is set, "Use Web Scraper" will auto-enable +1. Go to **Settings** > **Providers** +2. Enable **"OpenSubtitles.org"** (not OpenSubtitles.com, that's the API version) +3. Set the scraper service URL (or use the `OPENSUBTITLES_SCRAPER_URL` env var) 4. Save and test with a manual search ### Enabling AI Translation -1. Go to **Settings** โ†’ **Subtitles** โ†’ **Translating** -2. Select **"AI Subtitle Translator"** from the Translator dropdown +1. Go to **Settings** > **AI Translator** +2. Select **"AI Subtitle Translator"** as the translator engine 3. Enter your **OpenRouter API Key** (get one at [openrouter.ai/keys](https://openrouter.ai/keys)) -4. Choose your preferred **AI Model** (Gemini 2.5 Flash recommended) +4. Choose your preferred **AI Model** (Google: Gemini 2.5 Flash Lite Preview 09-2025 recommended) 5. Save and test with a manual translation ---- +
-## ๐Ÿ—๏ธ Architecture +
+Architecture ``` โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” @@ -228,7 +311,7 @@ docker compose up -d โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ Bazarr โ”‚ โ”‚ OpenSubtitles Scraper โ”‚ โ”‚ AI Sub โ”‚ โ”‚ -โ”‚ โ”‚ (LavX Fork) โ”‚ โ”‚ (Port 8000) โ”‚ โ”‚ Translator โ”‚ โ”‚ +โ”‚ โ”‚ (Bazarr+) โ”‚ โ”‚ (Port 8000) โ”‚ โ”‚ Translator โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ (Port 8765) โ”‚ โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ HTTP โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ OpenSubtitles.orgโ”‚โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”‚ Search API โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ @@ -247,30 +330,12 @@ docker compose up -d โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` ---- +> **Note:** The scraper service can optionally use [FlareSolverr](https://github.com/FlareSolverr/FlareSolverr) (port 8191) to handle browser challenges. See the Docker Compose example above for the full setup. -## ๐Ÿ› ๏ธ Configuration Options +
-### Environment Variables - -| Variable | Default | Description | -|----------|---------|-------------| -| `PUID` | `1000` | User ID for file permissions | -| `PGID` | `1000` | Group ID for file permissions | -| `TZ` | `UTC` | Timezone (e.g., `Europe/Budapest`) | -| `OPENSUBTITLES_SCRAPER_URL` | `http://opensubtitles-scraper:8765` | Scraper service URL | - -### Volumes - -| Path | Description | -|------|-------------| -| `/config` | Bazarr configuration and database | -| `/movies` | Movies library (match your Radarr path) | -| `/tv` | TV shows library (match your Sonarr path) | - ---- - -## ๐Ÿ”ง Troubleshooting +
+Troubleshooting ### Scraper Connection Issues @@ -293,116 +358,22 @@ curl -X POST http://localhost:8000/search \ |-------|----------| | "Connection refused" | Ensure scraper is running and healthy | | "No subtitles found" | Check IMDB ID is correct, try different language | -| Provider not showing | Enable it in Settings โ†’ Providers | +| Provider not showing | Enable it in Settings > Providers | | Wrong file permissions | Check PUID/PGID match your user | ---- - -## ๐Ÿ“š Documentation - -- [Fork Maintenance Guide](docs/FORK_MAINTENANCE.md) - How sync works -- [OpenSubtitles Scraper](https://github.com/LavX/opensubtitles-scraper) - Scraper docs -- [AI Subtitle Translator](https://github.com/LavX/ai-subtitle-translator) - AI translator docs -- [Bazarr Wiki](https://wiki.bazarr.media) - General Bazarr documentation - ---- - -## ๐Ÿค Contributing - -Contributions are welcome! Please: - -1. Fork this repository -2. Create a feature branch -3. Submit a pull request - -For major changes, please open an issue first. - ---- - -## ๐ŸŒ About the Maintainer - -This fork is maintained by **LavX**. Explore more of my projects and services: - -### ๐Ÿš€ Services -- **[LavX Managed Systems](https://lavx.hu)** โ€“ Enterprise AI solutions, RAG systems, and LLMOps. -- **[LavX News](https://news.lavx.hu)** โ€“ Latest insights on AI, Open Source, and emerging tech. -- **[LMS Tools](https://tools.lavx.hu)** โ€“ 140+ free, privacy-focused online tools for developers and researchers. - -### ๐Ÿ› ๏ธ Open Source Projects -- **[AI Subtitle Translator](https://github.com/LavX/ai-subtitle-translator)** โ€“ LLM-powered subtitle translator using OpenRouter API. -- **[OpenSubtitles Scraper](https://github.com/LavX/opensubtitles-scraper)** โ€“ Web scraper for OpenSubtitles.org (no VIP required). -- **[JFrog to Nexus OSS](https://github.com/LavX/jfrogtonexusoss)** โ€“ Automated migration tool for repository managers. -- **[WeatherFlow](https://github.com/LavX/weatherflow)** โ€“ Multi-platform weather data forwarding (WU to Windy/Idokep). -- **[Like4Like Suite](https://github.com/LavX/Like4Like-Suite)** โ€“ Social media automation and engagement toolkit. - ---- - -## ๐Ÿ“„ License - -- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html) -- Original Bazarr Copyright 2010-2025 morpheus65535 -- Fork modifications Copyright 2025 LavX - ---- +
-

๐Ÿ“œ Original Bazarr README

- -# bazarr - -Bazarr is a companion application to Sonarr and Radarr. It manages and downloads subtitles based on your requirements. You define your preferences by TV show or movie and Bazarr takes care of everything for you. - -Be aware that Bazarr doesn't scan disk to detect series and movies: It only takes care of the series and movies that are indexed in Sonarr and Radarr. - -Thanks to the folks at OpenSubtitles for their logo that was an inspiration for ours. +Supported Subtitle Providers -## Support on Paypal - -At the request of some, here is a way to demonstrate your appreciation for the efforts made in the development of Bazarr: -[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=XHHRWXT9YB7WE&source=url) - -## Status (LavX Fork) - -[![GitHub issues](https://img.shields.io/github/issues/LavX/bazarr.svg?style=flat-square)](https://github.com/LavX/bazarr/issues) -[![GitHub stars](https://img.shields.io/github/stars/LavX/bazarr.svg?style=flat-square)](https://github.com/LavX/bazarr/stargazers) -[![GitHub forks](https://img.shields.io/github/forks/LavX/bazarr.svg?style=flat-square)](https://github.com/LavX/bazarr/network) -[![Docker Build](https://img.shields.io/github/actions/workflow/status/LavX/bazarr/build-docker.yml?style=flat-square&label=docker)](https://github.com/LavX/bazarr/actions/workflows/build-docker.yml) -## Support - -For installation and configuration instructions, see upstream [wiki](https://wiki.bazarr.media). - -For fork-specific issues (OpenSubtitles scraper), open an issue on [this fork](https://github.com/LavX/bazarr/issues). - -For general Bazarr issues, please use the [upstream repo](https://github.com/morpheus65535/bazarr/issues). - -Original Bazarr Discord: [![Discord](https://img.shields.io/badge/discord-chat-MH2e2eb.svg?style=flat-square)](https://discord.gg/MH2e2eb) - -## Feature Requests - -If you need something that is not already part of Bazarr, feel free to create a feature request on [Feature Upvote](http://features.bazarr.media). - -## Major Features Include: - -- Support for major platforms: Windows, Linux, macOS, Raspberry Pi, etc. -- Automatically add new series and episodes from Sonarr -- Automatically add new movies from Radarr -- Series or movies based configuration for subtitles languages -- Scan your existing library for internal and external subtitles and download any missing -- Keep history of what was downloaded from where and when -- Manual search so you can download subtitles on demand -- Upgrade subtitles previously downloaded when a better one is found -- Ability to delete external subtitles from disk -- Currently support 184 subtitles languages with support for forced/foreign subtitles (depending of providers) -- And a beautiful UI based on Sonarr - -## Supported subtitles providers: +Includes all upstream providers plus fork additions: - Addic7ed - AnimeKalesi -- Animetosho (requires AniDb HTTP API client described [here](https://wiki.anidb.net/HTTP_API_Definition)) +- Animetosho (requires [AniDb HTTP API client](https://wiki.anidb.net/HTTP_API_Definition)) - AnimeSub.info - Assrt -- AvistaZ, CinemaZ (Get session cookies using method described [here](https://github.com/morpheus65535/bazarr/pull/2375#issuecomment-2057010996)) +- AvistaZ, CinemaZ - BetaSeries - BSplayer - Embedded Subtitles @@ -419,7 +390,7 @@ If you need something that is not already part of Bazarr, feel free to create a - Napisy24 - Nekur - OpenSubtitles.com -- OpenSubtitles.org (LavX Fork) +- **OpenSubtitles.org (Bazarr+ web scraper, no API needed)** - Podnapisi - RegieLive - Sous-Titres.eu @@ -443,15 +414,55 @@ If you need something that is not already part of Bazarr, feel free to create a - Turkcealtyazi.org - TuSubtitulo - TVSubtitles -- Whisper (requires [ahmetoner/whisper-asr-webservice](https://github.com/ahmetoner/whisper-asr-webservice)) +- Whisper (requires [whisper-asr-webservice](https://github.com/ahmetoner/whisper-asr-webservice)) - Wizdom - XSubs - Yavka.net - YIFY Subtitles - Zimuku -## Screenshot +
+ +
+About the Maintainer + +This fork is maintained by **LavX**. Explore more projects and services: + +### Services +- **[LavX Managed Systems](https://lavx.hu)** -- Enterprise AI solutions, RAG systems, and LLMOps. +- **[LavX News](https://news.lavx.hu)** -- Latest insights on AI, Open Source, and emerging tech. +- **[LMS Tools](https://tools.lavx.hu)** -- 140+ free, privacy-focused online tools for developers and researchers. -![Bazarr](/screenshot/bazarr-screenshot.png?raw=true "Bazarr") +### Open Source Projects +- **[AI Subtitle Translator](https://github.com/LavX/ai-subtitle-translator)** -- LLM-powered subtitle translator using OpenRouter API. +- **[OpenSubtitles Scraper](https://github.com/LavX/opensubtitles-scraper)** -- Web scraper for OpenSubtitles.org (no VIP required). +- **[JFrog to Nexus OSS](https://github.com/LavX/jfrogtonexusoss)** -- Automated migration tool for repository managers. +- **[WeatherFlow](https://github.com/LavX/weatherflow)** -- Multi-platform weather data forwarding (WU to Windy/Idokep). +- **[Like4Like Suite](https://github.com/LavX/Like4Like-Suite)** -- Social media automation and engagement toolkit. -
\ No newline at end of file + + +--- + +## Documentation + +- [Fork Maintenance Guide](docs/FORK_MAINTENANCE.md) - How sync works +- [OpenSubtitles Scraper](https://github.com/LavX/opensubtitles-scraper) - Scraper docs +- [AI Subtitle Translator](https://github.com/LavX/ai-subtitle-translator) - AI translator docs +- [Bazarr Wiki](https://wiki.bazarr.media) - General Bazarr documentation + +## Contributing + +Contributions are welcome! See [CONTRIBUTING.md](CONTRIBUTING.md) for the full guide. + +1. Fork this repository +2. Create a feature branch from `development` +3. Submit a PR targeting `development` + +For major changes, please open an issue first to discuss. + +## License + +- [GNU GPL v3](http://www.gnu.org/licenses/gpl.html) +- Based on [Bazarr](https://github.com/morpheus65535/bazarr) by morpheus65535 +- Fork modifications Copyright 2025-2026 LavX diff --git a/ai-subtitle-translator b/ai-subtitle-translator index 92b9522343..1ef643b7a9 160000 --- a/ai-subtitle-translator +++ b/ai-subtitle-translator @@ -1 +1 @@ -Subproject commit 92b9522343b494b428dc8e0df2b09b6ff87b4e0b +Subproject commit 1ef643b7a93a977879230526655a2a61817e4b6a diff --git a/bazarr-binaries b/bazarr-binaries new file mode 160000 index 0000000000..0710cde74a --- /dev/null +++ b/bazarr-binaries @@ -0,0 +1 @@ +Subproject commit 0710cde74a5d87975119fc7ad3d6d80a20b6fed5 diff --git a/bazarr.py b/bazarr.py index e5023714de..df9cdbd2ea 100644 --- a/bazarr.py +++ b/bazarr.py @@ -8,7 +8,7 @@ import time from bazarr.app.get_args import args -from bazarr.literals import EXIT_PYTHON_UPGRADE_NEEDED, EXIT_NORMAL, FILE_RESTART, FILE_STOP, ENV_RESTARTFILE, ENV_STOPFILE, EXIT_INTERRUPT +from bazarr.literals import EXIT_PYTHON_UPGRADE_NEEDED, EXIT_NORMAL, FILE_RESTART, FILE_STOP, ENV_RESTARTFILE, ENV_STOPFILE, EXIT_INTERRUPT, EXIT_UNEXPECTED_ERROR # always flush print statements sys.stdout.reconfigure(line_buffering=True) @@ -27,8 +27,8 @@ def check_python_version(): print("Python " + minimum_py3_str + " or greater required. " "Current version is " + platform.python_version() + ". Please upgrade Python.") exit_program(EXIT_PYTHON_UPGRADE_NEEDED) - elif int(python_version[0]) == 3 and int(python_version[1]) > 13: - print("Python version greater than 3.13.x is unsupported. Current version is " + platform.python_version() + + elif int(python_version[0]) == 3 and int(python_version[1]) > 14: + print("Python version greater than 3.14.x is unsupported. Current version is " + platform.python_version() + ". Keep in mind that even if it works, you're on your own.") elif (int(python_version[0]) == minimum_py3_tuple[0] and int(python_version[1]) < minimum_py3_tuple[1]) or \ (int(python_version[0]) != minimum_py3_tuple[0]): @@ -106,27 +106,21 @@ def check_status(): os.remove(restart_file) except Exception: print('Unable to delete restart file.') - finally: - terminate_child() - print("Bazarr is restarting...") - child_process = start_bazarr() - + terminate_child() + print("Bazarr is restarting...") + child_process = start_bazarr() + return + + if not is_process_running(child_process): + print("Bazarr child process has stopped unexpectedly. Shutting down...") + exit_program(EXIT_UNEXPECTED_ERROR) -def is_process_running(pid): - commands = { - "win": ["tasklist", "/FI", f"PID eq {pid}"], - "linux": ["ps", "-eo", "pid"], - "darwin": ["ps", "-ax", "-o", "pid"] - } - # Determine OS and execute corresponding command - for key in commands: - if sys.platform.startswith(key): - result = subprocess.run(commands[key], capture_output=True, text=True) - return str(pid) in result.stdout.split() +def is_process_running(child_process): + status = child_process.poll() + # status is exit code if process has stopped, or None if it's still running + return status is None - print("Unsupported OS") - return False def interrupt_handler(signum, frame): # catch and ignore keyboard interrupt Ctrl-C @@ -137,7 +131,7 @@ def interrupt_handler(signum, frame): interrupted = True print('Handling keyboard interrupt...') else: - if not is_process_running(child_process.pid): + if not is_process_running(child_process): # this will be caught by the main loop below raise SystemExit(EXIT_INTERRUPT) diff --git a/bazarr/api/episodes/history.py b/bazarr/api/episodes/history.py index bc7ab6a31d..ecd535fb34 100644 --- a/bazarr/api/episodes/history.py +++ b/bazarr/api/episodes/history.py @@ -82,6 +82,7 @@ def get(self): TableEpisodes.path, TableHistory.language, TableHistory.score, + TableHistory.score_out_of, TableShows.tags, TableHistory.action, TableHistory.video_path, @@ -117,6 +118,7 @@ def get(self): 'language': x.language, 'profileId': x.profileId, 'score': x.score, + 'score_out_of': x.score_out_of, 'tags': x.tags, 'action': x.action, 'video_path': x.video_path, @@ -154,7 +156,7 @@ def get(self): del item['profileId'] if item['score']: - item['score'] = f"{round((int(item['score']) * 100 / 360), 2)}%" + item['score'] = f"{round((int(item['score']) * 100 / item['score_out_of']), 2)}%" # Make timestamp pretty if item['timestamp']: diff --git a/bazarr/api/movies/history.py b/bazarr/api/movies/history.py index 2324fc73f3..8c7f59fcfc 100644 --- a/bazarr/api/movies/history.py +++ b/bazarr/api/movies/history.py @@ -80,6 +80,7 @@ def get(self): TableHistoryMovie.language, TableMovies.tags, TableHistoryMovie.score, + TableHistoryMovie.score_out_of, TableHistoryMovie.subs_id, TableHistoryMovie.provider, TableHistoryMovie.subtitles_path, @@ -110,6 +111,7 @@ def get(self): 'profileId': x.profileId, 'tags': x.tags, 'score': x.score, + 'score_out_of': x.score_out_of, 'subs_id': x.subs_id, 'provider': x.provider, 'subtitles_path': x.subtitles_path, @@ -145,7 +147,7 @@ def get(self): del item['profileId'] if item['score']: - item['score'] = f"{round((int(item['score']) * 100 / 120), 2)}%" + item['score'] = f"{round((int(item['score']) * 100 / item['score_out_of']), 2)}%" # Make timestamp pretty if item['timestamp']: diff --git a/bazarr/api/plex/oauth.py b/bazarr/api/plex/oauth.py index b72ee7f6d1..1e198bb9bd 100644 --- a/bazarr/api/plex/oauth.py +++ b/bazarr/api/plex/oauth.py @@ -18,6 +18,7 @@ from .exceptions import * from .security import (TokenManager, sanitize_log_data, pin_cache, get_or_create_encryption_key, sanitize_server_url, encrypt_api_key) +from app.config import get_ssl_verify from app.config import settings, write_config from app.logger import logger from utilities.plex_utils import _get_library_locations @@ -223,7 +224,7 @@ def test_plex_connection(uri, token): f"{uri}/identity", headers=headers, timeout=3, - verify=False + verify=get_ssl_verify('plex') ) latency_ms = int((time.time() - start_time) * 1000) @@ -318,10 +319,11 @@ def get(self, pin_id): if not cached_pin: raise PlexPinExpiredError("PIN not found or expired") - if state_param: - stored_state = cached_pin.get('state_token') - if not stored_state or not get_token_manager().validate_state_token(state_param, stored_state): + stored_state = cached_pin.get('state_token') + if stored_state: + if not state_param or not get_token_manager().validate_state_token(state_param, stored_state): logger.warning(f"CSRF state validation failed for PIN {pin_id}") + abort(403, "Request validation failed") headers = { 'Accept': 'application/json', @@ -625,7 +627,7 @@ def get(self): f"{server_url}/library/sections", headers=headers, timeout=10, - verify=False + verify=get_ssl_verify('plex') ) if lib_response.status_code in (401, 403): @@ -691,7 +693,7 @@ def get(self): f"{successful_server_url}/library/sections/{section_key}/all", headers={'X-Plex-Token': decrypted_token, 'Accept': 'application/json'}, timeout=5, - verify=False + verify=get_ssl_verify('plex') ) actual_count = 0 @@ -840,7 +842,7 @@ def post(self): f"{uri}/identity", headers=headers, timeout=3, - verify=False + verify=get_ssl_verify('plex') ) if response.status_code == 200: diff --git a/bazarr/api/providers/providers_episodes.py b/bazarr/api/providers/providers_episodes.py index 38a1650ba1..3b75593686 100644 --- a/bazarr/api/providers/providers_episodes.py +++ b/bazarr/api/providers/providers_episodes.py @@ -95,7 +95,7 @@ def get(self): post_request_parser.add_argument('original_format', type=str, required=True, help='Use original subtitles format from ["True", "False"]') post_request_parser.add_argument('provider', type=str, required=True, help='Provider name') - post_request_parser.add_argument('subtitle', type=str, required=True, help='Pickled subtitles as return by GET') + post_request_parser.add_argument('subtitle', type=str, required=True, help='Subtitle ID as returned by GET') @authenticate @api_ns_providers_episodes.doc(parser=post_request_parser) diff --git a/bazarr/api/providers/providers_movies.py b/bazarr/api/providers/providers_movies.py index 344411a7e0..d41124f4d7 100644 --- a/bazarr/api/providers/providers_movies.py +++ b/bazarr/api/providers/providers_movies.py @@ -93,7 +93,7 @@ def get(self): post_request_parser.add_argument('original_format', type=str, required=True, help='Use original subtitles format from ["True", "False"]') post_request_parser.add_argument('provider', type=str, required=True, help='Provider name') - post_request_parser.add_argument('subtitle', type=str, required=True, help='Pickled subtitles as return by GET') + post_request_parser.add_argument('subtitle', type=str, required=True, help='Subtitle ID as returned by GET') @authenticate @api_ns_providers_movies.doc(parser=post_request_parser) diff --git a/bazarr/api/subtitles/__init__.py b/bazarr/api/subtitles/__init__.py index b107acc414..b20653788d 100644 --- a/bazarr/api/subtitles/__init__.py +++ b/bazarr/api/subtitles/__init__.py @@ -2,11 +2,15 @@ from .subtitles import api_ns_subtitles from .subtitles_info import api_ns_subtitles_info -from .batch_translate import api_ns_batch_translate +from .batch import api_ns_batch +from .content import api_ns_subtitle_content +from .subtitles_contents import api_ns_subtitle_contents api_ns_list_subtitles = [ api_ns_subtitles, api_ns_subtitles_info, - api_ns_batch_translate, + api_ns_batch, + api_ns_subtitle_content, + api_ns_subtitle_contents, ] diff --git a/bazarr/api/subtitles/batch.py b/bazarr/api/subtitles/batch.py new file mode 100644 index 0000000000..f0f0615ff5 --- /dev/null +++ b/bazarr/api/subtitles/batch.py @@ -0,0 +1,205 @@ +# coding=utf-8 + +import logging +from flask_restx import Resource, Namespace, fields +from sqlalchemy import and_, or_, func + +from app.config import settings +from app.database import TableHistory, TableHistoryMovie, TableEpisodes, TableMovies, database, select +from app.jobs_queue import jobs_queue +from subtitles.mass_operations import mass_batch_operation, VALID_ACTIONS +from ..utils import authenticate + +ACTION_LABELS = { + 'sync': 'Syncing Subtitles', + 'translate': 'Translating Subtitles', + 'OCR_fixes': 'Applying OCR Fixes', + 'common': 'Applying Common Fixes', + 'remove_HI': 'Removing Hearing Impaired Tags', + 'remove_tags': 'Removing Style Tags', + 'fix_uppercase': 'Fixing Uppercase', + 'reverse_rtl': 'Reversing RTL', + 'emoji': 'Removing Emoji', + 'scan-disk': 'Scanning Disk', + 'search-missing': 'Searching Missing Subtitles', + 'upgrade': 'Upgrading Subtitles', +} + +logger = logging.getLogger(__name__) + +api_ns_batch = Namespace('Batch', description='Unified batch subtitle operations') + + +@api_ns_batch.route('subtitles/batch') +class BatchOperation(Resource): + post_item_model = api_ns_batch.model('BatchItem', { + 'type': fields.String(required=True, description='Type: "episode", "movie", or "series"'), + 'sonarrSeriesId': fields.Integer(description='Sonarr Series ID'), + 'sonarrEpisodeId': fields.Integer(description='Sonarr Episode ID'), + 'radarrId': fields.Integer(description='Radarr Movie ID'), + }) + + post_options_model = api_ns_batch.model('BatchOptions', { + 'max_offset_seconds': fields.Integer(default=60), + 'no_fix_framerate': fields.Boolean(default=True), + 'gss': fields.Boolean(default=True), + 'force_resync': fields.Boolean(default=False), + 'from_lang': fields.String(description='Source language code for translate action'), + 'to_lang': fields.String(description='Target language code for translate action'), + }) + + post_request_model = api_ns_batch.model('BatchRequest', { + 'items': fields.List(fields.Nested(post_item_model), required=True), + 'action': fields.String(required=True, description=f'Action: one of {", ".join(sorted(VALID_ACTIONS))}'), + 'options': fields.Nested(post_options_model), + }) + + post_response_model = api_ns_batch.model('BatchResponse', { + 'queued': fields.Integer(description='Number of items processed'), + 'skipped': fields.Integer(description='Number of items skipped'), + 'errors': fields.List(fields.String(), description='Error messages'), + }) + + @authenticate + @api_ns_batch.response(200, 'Success', post_response_model) + @api_ns_batch.response(400, 'Bad Request') + @api_ns_batch.response(401, 'Not Authenticated') + def post(self): + """Execute a batch operation on multiple items""" + from flask import request + data = request.get_json() + + if not data: + return {'error': 'No data provided'}, 400 + + action = data.get('action') + if not action or action not in VALID_ACTIONS: + return {'error': f'Invalid action. Must be one of: {", ".join(sorted(VALID_ACTIONS))}'}, 400 + + items = data.get('items') + if items is None: + return {'error': 'No items provided'}, 400 + + if not isinstance(items, list): + return {'error': 'items must be a list'}, 400 + + if not items: + return {'error': 'Empty items list'}, 400 + + VALID_ITEM_KEYS = {'type', 'sonarrSeriesId', 'sonarrEpisodeId', 'radarrId'} + VALID_TYPES = {'episode', 'movie', 'series'} + + sanitized_items = [] + for item in items: + if not isinstance(item, dict) or item.get('type') not in VALID_TYPES: + continue + sanitized_items.append({k: v for k, v in item.items() if k in VALID_ITEM_KEYS}) + items = sanitized_items + + if not items: + return {'error': 'No valid items after sanitization'}, 400 + + MAX_BATCH_SIZE = 10000 + + if len(items) > MAX_BATCH_SIZE: + return {'error': f'Batch size exceeds maximum of {MAX_BATCH_SIZE}'}, 400 + + options = data.get('options', {}) + + label = ACTION_LABELS.get(action, f'Batch {action}') + job_name = f"{label} ({len(items)} items)" + + job_id = jobs_queue.feed_jobs_pending_queue( + job_name=job_name, + module='subtitles.mass_operations', + func='mass_batch_operation', + kwargs={ + 'items': items, + 'action': action, + 'options': options, + }, + is_progress=True, + ) + + return {'queued': len(items), 'skipped': 0, 'errors': [], 'job_id': job_id}, 200 + + +def get_upgradable_media_ids(): + """Return sets of radarrIds and sonarrSeriesIds that have upgradable subtitles. + + Uses the same latest-row-per-video/language logic as upgrade.py to avoid + false positives from old history entries that have already been superseded. + """ + if not settings.general.upgrade_subs: + return {'movies': [], 'series': []} + + from subtitles.upgrade import get_queries_condition_parameters + minimum_timestamp, query_actions = get_queries_condition_parameters() + + # Movies: only consider the latest history row per (video_path, language) + max_movie_ts = select( + TableHistoryMovie.video_path, + TableHistoryMovie.language, + func.max(TableHistoryMovie.timestamp).label('timestamp') + ).group_by( + TableHistoryMovie.video_path, TableHistoryMovie.language + ).distinct().subquery() + + movie_results = database.execute( + select(TableHistoryMovie.radarrId) + .distinct() + .join(TableMovies, TableHistoryMovie.radarrId == TableMovies.radarrId) + .join(max_movie_ts, onclause=and_( + TableHistoryMovie.video_path == max_movie_ts.c.video_path, + TableHistoryMovie.language == max_movie_ts.c.language, + TableHistoryMovie.timestamp == max_movie_ts.c.timestamp, + )) + .where(and_( + TableHistoryMovie.action.in_(query_actions), + TableHistoryMovie.timestamp > minimum_timestamp, + or_( + and_(TableHistoryMovie.score.is_(None), TableHistoryMovie.action == 6), + TableHistoryMovie.score < TableHistoryMovie.score_out_of - 3 + ) + )) + ).all() + movie_ids = [r.radarrId for r in movie_results] + + # Series: only consider the latest history row per (video_path, language) + max_episode_ts = select( + TableHistory.video_path, + TableHistory.language, + func.max(TableHistory.timestamp).label('timestamp') + ).group_by( + TableHistory.video_path, TableHistory.language + ).distinct().subquery() + + series_results = database.execute( + select(TableHistory.sonarrSeriesId) + .distinct() + .join(TableEpisodes, TableHistory.sonarrEpisodeId == TableEpisodes.sonarrEpisodeId) + .join(max_episode_ts, onclause=and_( + TableHistory.video_path == max_episode_ts.c.video_path, + TableHistory.language == max_episode_ts.c.language, + TableHistory.timestamp == max_episode_ts.c.timestamp, + )) + .where(and_( + TableHistory.action.in_(query_actions), + TableHistory.timestamp > minimum_timestamp, + or_( + and_(TableHistory.score.is_(None), TableHistory.action == 6), + TableHistory.score < TableHistory.score_out_of - 3 + ) + )) + ).all() + series_ids = [r.sonarrSeriesId for r in series_results] + + return {'movies': movie_ids, 'series': series_ids} + + +@api_ns_batch.route('subtitles/upgradable') +class UpgradableMedia(Resource): + @authenticate + def get(self): + """Return movie and series IDs that have upgradable subtitles""" + return get_upgradable_media_ids(), 200 diff --git a/bazarr/api/subtitles/batch_translate.py b/bazarr/api/subtitles/batch_translate.py deleted file mode 100644 index b6156b2c28..0000000000 --- a/bazarr/api/subtitles/batch_translate.py +++ /dev/null @@ -1,113 +0,0 @@ -# coding=utf-8 - -import logging -from flask_restx import Resource, Namespace, reqparse, fields - -from app.jobs_queue import jobs_queue -from ..utils import authenticate - -logger = logging.getLogger(__name__) - -api_ns_batch_translate = Namespace('BatchTranslate', description='Batch translate subtitles') - - -@api_ns_batch_translate.route('subtitles/translate/batch') -class BatchTranslate(Resource): - post_request_parser = reqparse.RequestParser() - post_request_parser.add_argument('items', type=list, location='json', required=True, - help='List of items to translate') - - post_item_model = api_ns_batch_translate.model('BatchTranslateItem', { - 'type': fields.String(required=True, description='Type: "episode" or "movie"'), - 'sonarrSeriesId': fields.Integer(description='Sonarr Series ID (for episodes)'), - 'sonarrEpisodeId': fields.Integer(description='Sonarr Episode ID (for episodes)'), - 'radarrId': fields.Integer(description='Radarr Movie ID (for movies)'), - 'sourceLanguage': fields.String(required=True, description='Source language code (e.g., "en")'), - 'targetLanguage': fields.String(required=True, description='Target language code (e.g., "hu")'), - 'subtitlePath': fields.String(description='Optional specific subtitle path to translate'), - 'forced': fields.Boolean(default=False, description='Forced subtitle flag'), - 'hi': fields.Boolean(default=False, description='Hearing impaired flag'), - }) - - post_request_model = api_ns_batch_translate.model('BatchTranslateRequest', { - 'items': fields.List(fields.Nested(post_item_model), required=True), - }) - - post_response_model = api_ns_batch_translate.model('BatchTranslateResponse', { - 'queued': fields.Integer(description='Number of items queued'), - 'skipped': fields.Integer(description='Number of items skipped'), - 'errors': fields.List(fields.String(), description='Error messages'), - }) - - @authenticate - @api_ns_batch_translate.doc(body=post_request_model) - @api_ns_batch_translate.response(200, 'Success', post_response_model) - @api_ns_batch_translate.response(400, 'Bad Request') - @api_ns_batch_translate.response(401, 'Not Authenticated') - def post(self): - """Queue batch translation jobs for multiple items""" - from flask import request - data = request.get_json() - - if not data or 'items' not in data: - return {'error': 'No items provided'}, 400 - - items = data.get('items', []) - if not items: - return {'error': 'Empty items list'}, 400 - - queued = 0 - skipped = 0 - errors = [] - - for item in items: - try: - item_type = item.get('type') - source_language = item.get('sourceLanguage') - target_language = item.get('targetLanguage') - forced = item.get('forced', False) - hi = item.get('hi', False) - subtitle_path = item.get('subtitlePath') - - if not item_type or not source_language or not target_language: - errors.append(f'Missing required fields in item: {item}') - skipped += 1 - continue - - if item_type == 'episode': - job_name = f"Translate Episode {item.get('sonarrEpisodeId')}" - func = 'process_episode_translation' - elif item_type == 'movie': - job_name = f"Translate Movie {item.get('radarrId')}" - func = 'process_movie_translation' - else: - errors.append(f'Invalid type "{item_type}" in item') - skipped += 1 - continue - - # Queue the job individually - jobs_queue.feed_jobs_pending_queue( - job_name=job_name, - module='subtitles.tools.translate.batch', - func=func, - kwargs={ - 'item': item, - 'source_language': source_language, - 'target_language': target_language, - 'forced': forced, - 'hi': hi, - 'subtitle_path': subtitle_path - } - ) - queued += 1 - - except Exception as e: - logger.error(f'Error queuing batch translate item: {e}', exc_info=True) - errors.append(str(e)) - skipped += 1 - - return { - 'queued': queued, - 'skipped': skipped, - 'errors': errors - }, 200 diff --git a/bazarr/api/subtitles/content.py b/bazarr/api/subtitles/content.py new file mode 100644 index 0000000000..e3afdc8800 --- /dev/null +++ b/bazarr/api/subtitles/content.py @@ -0,0 +1,226 @@ +# coding=utf-8 + +import ast +import hashlib +import os + +from flask import make_response, jsonify, request +from flask_restx import Resource, Namespace + +from app.database import TableEpisodes, TableMovies, TableShows, database, select +from utilities.path_mappings import path_mappings + +from ..utils import authenticate + +api_ns_subtitle_content = Namespace('SubtitleContent', description='Read subtitle file content') + +MAX_FILE_SIZE = 5 * 1024 * 1024 # 5 MB + +SUBTITLE_EXTENSIONS = { + '.srt', '.ass', '.ssa', '.sub', '.idx', '.sup', + '.vtt', '.dfxp', '.ttml', '.smi', '.mpl', '.txt', +} + +FORMAT_MAP = { + '.srt': 'srt', + '.ass': 'ass', + '.ssa': 'ssa', + '.vtt': 'vtt', + '.sub': 'sub', + '.dfxp': 'dfxp', + '.ttml': 'ttml', + '.smi': 'smi', + '.mpl': 'mpl', + '.txt': 'txt', +} + + +def resolve_subtitle_path(media_type, media_id, language_code): + """Resolve a subtitle file path from a media ID and language code. + + language_code can be like "en", "hu", "en:hi", "en:forced". + Matches against the language field in the subtitles array. + + Returns (path, language, metadata) on success, or (message, status_code) on failure. + """ + metadata = {} + if media_type == 'episode': + row = database.execute( + select(TableEpisodes.subtitles, TableEpisodes.path, TableEpisodes.sonarrSeriesId, TableEpisodes.title) + .where(TableEpisodes.sonarrEpisodeId == media_id) + ).first() + if row: + series_row = database.execute( + select(TableShows.title).where(TableShows.sonarrSeriesId == row.sonarrSeriesId) + ).first() + metadata = { + 'mediaTitle': series_row.title if series_row else None, + 'mediaId': row.sonarrSeriesId, + 'episodeTitle': row.title, + } + elif media_type == 'movie': + row = database.execute( + select(TableMovies.subtitles, TableMovies.path, TableMovies.title, TableMovies.radarrId) + .where(TableMovies.radarrId == media_id) + ).first() + if row: + metadata = { + 'mediaTitle': row.title, + 'mediaId': row.radarrId, + } + else: + return 'Invalid media type', 400 + + if not row: + return 'Media not found', 404 + + raw_subtitles = row.subtitles + if not raw_subtitles: + return 'No subtitles found for this media', 404 + + try: + subtitles_list = ast.literal_eval(raw_subtitles) + except (ValueError, SyntaxError): + return 'Failed to parse subtitles data', 500 + + if not isinstance(subtitles_list, list): + return 'Invalid subtitles data', 500 + + # Find the subtitle entry matching the language code + entry = None + for item in subtitles_list: + if isinstance(item, list) and len(item) >= 2 and item[0] == language_code: + # Must have a file path (not an embedded track) + if item[1] and isinstance(item[1], str) and len(item[1]) > 0: + entry = item + break + + if entry is None: + return f'No subtitle found for language "{language_code}"', 404 + + language = entry[0] + subtitle_path = entry[1] + + if media_type == 'episode': + subtitle_path = path_mappings.path_replace(subtitle_path) + else: + subtitle_path = path_mappings.path_replace_movie(subtitle_path) + + ext = os.path.splitext(subtitle_path)[1].lower() + if ext not in SUBTITLE_EXTENSIONS: + return f'File does not have a recognized subtitle extension: {ext}', 400 + + if not os.path.isfile(subtitle_path): + return 'Subtitle file not found on disk', 404 + + return subtitle_path, language, metadata + + +def read_subtitle_file(path): + """Read a subtitle file and detect its encoding. + + Returns (content_str, encoding) on success. + Raises ValueError if the file exceeds MAX_FILE_SIZE. + """ + file_size = os.path.getsize(path) + if file_size > MAX_FILE_SIZE: + raise ValueError(f'Subtitle file too large ({file_size} bytes, max {MAX_FILE_SIZE})') + + with open(path, 'rb') as f: + raw = f.read() + + # BOM detection + if raw.startswith(b'\xef\xbb\xbf'): + return raw[3:].decode('utf-8'), 'utf-8-sig' + if raw.startswith(b'\xff\xfe'): + return raw[2:].decode('utf-16-le'), 'utf-16-le' + if raw.startswith(b'\xfe\xff'): + return raw[2:].decode('utf-16-be'), 'utf-16-be' + + # Try UTF-8 + try: + return raw.decode('utf-8'), 'utf-8' + except UnicodeDecodeError: + pass + + # charset_normalizer + try: + import charset_normalizer + result = charset_normalizer.from_bytes(raw).best() + if result is not None: + return str(result), result.encoding + except Exception: + pass + + # Fallback + return raw.decode('cp1252', errors='replace'), 'cp1252' + + +def detect_subtitle_format(path): + """Map a subtitle file extension to a format string.""" + ext = os.path.splitext(path)[1].lower() + return FORMAT_MAP.get(ext, 'unknown') + + +def generate_etag(path): + """Generate an ETag from file mtime and size.""" + stat = os.stat(path) + tag_input = f"{stat.st_mtime_ns}:{stat.st_size}" + return hashlib.md5(tag_input.encode()).hexdigest() + + +def _get_subtitle_content(media_type, media_id, language_code): + """Shared handler for episode and movie subtitle content.""" + result = resolve_subtitle_path(media_type, media_id, language_code) + + # Error tuple + if isinstance(result[1], int): + return result[0], result[1] + + subtitle_path, language, metadata = result + + etag = generate_etag(subtitle_path) + + # ETag-based caching + if_none_match = request.headers.get('If-None-Match') + if if_none_match and if_none_match.strip('"') == etag: + return '', 304 + + try: + content, encoding = read_subtitle_file(subtitle_path) + except ValueError as e: + return str(e), 413 + + stat = os.stat(subtitle_path) + fmt = detect_subtitle_format(subtitle_path) + + response_data = { + 'content': content, + 'encoding': encoding, + 'format': fmt, + 'language': language, + 'size': stat.st_size, + 'lastModified': stat.st_mtime, + } + response_data.update(metadata) + + response = make_response(jsonify(response_data)) + + response.headers['ETag'] = f'"{etag}"' + response.headers['X-Content-Type-Options'] = 'nosniff' + + return response + + +@api_ns_subtitle_content.route('episodes//subtitles//content') +class EpisodeSubtitleContent(Resource): + @authenticate + def get(self, sonarrEpisodeId, language): + return _get_subtitle_content('episode', sonarrEpisodeId, language) + + +@api_ns_subtitle_content.route('movies//subtitles//content') +class MovieSubtitleContent(Resource): + @authenticate + def get(self, radarrId, language): + return _get_subtitle_content('movie', radarrId, language) diff --git a/bazarr/api/subtitles/subtitles.py b/bazarr/api/subtitles/subtitles.py index 36f5aa90c9..ea9042ed07 100644 --- a/bazarr/api/subtitles/subtitles.py +++ b/bazarr/api/subtitles/subtitles.py @@ -146,6 +146,7 @@ def patch(self): if action == 'sync': try: + postprocess_callback = lambda: postprocess_subtitles(subtitles_path, video_path, media_type, metadata, id) sync_subtitles(video_path=video_path, srt_path=subtitles_path, srt_lang=language, hi=hi, forced=forced, percent_score=0, # make sure to always sync when requested manually reference=args.get('reference') if args.get('reference') not in empty_values else @@ -159,6 +160,7 @@ def patch(self): sonarr_episode_id=id if media_type == "episode" else None, radarr_id=id if media_type == "movie" else None, force_sync=True, + callback=postprocess_callback ) except OSError: return 'Unable to edit subtitles file. Check logs.', 409 @@ -189,38 +191,42 @@ def patch(self): media_type="series" if media_type == "episode" else "movies", sonarr_series_id=metadata.sonarrSeriesId if media_type == "episode" else None, sonarr_episode_id=id, - radarr_id=id) + radarr_id=id, + metadata=metadata) + except OSError: return 'Unable to edit subtitles file. Check logs.', 409 else: try: subtitles_apply_mods(language=language, subtitle_path=subtitles_path, mods=[action], video_path=video_path) + postprocess_subtitles(subtitles_path, video_path, media_type, metadata, id) except OSError: return 'Unable to edit subtitles file. Check logs.', 409 - # apply chmod if required - chmod = int(settings.general.chmod, 8) if not sys.platform.startswith( - 'win') and settings.general.chmod_enabled else None - if chmod: - os.chmod(subtitles_path, chmod) + return '', 204 - if media_type == 'episode': - store_subtitles(path_mappings.path_replace_reverse(video_path), video_path) - event_stream(type='series', payload=metadata.sonarrSeriesId) - event_stream(type='episode', payload=id) - if settings.general.use_plex and settings.plex.update_series_library: - plex_refresh_item(metadata.imdbId, is_movie=False, season=metadata.season, - episode=metadata.episode) - else: - store_subtitles_movie(path_mappings.path_replace_reverse_movie(video_path), video_path) - event_stream(type='movie', payload=id) +def postprocess_subtitles(subtitles_path, video_path, media_type, metadata, id): + # apply chmod if required + chmod = int(settings.general.chmod, 8) if not sys.platform.startswith('win') and settings.general.chmod_enabled else None + if chmod: + os.chmod(subtitles_path, chmod) - if settings.general.use_plex and settings.plex.update_movie_library: - plex_refresh_item(metadata.imdbId, is_movie=True) + if media_type == 'episode': + store_subtitles(path_mappings.path_replace_reverse(video_path), video_path) + event_stream(type='series', payload=metadata.sonarrSeriesId) + event_stream(type='episode', payload=id) - return '', 204 + if settings.general.use_plex and settings.plex.update_series_library: + plex_refresh_item(metadata.imdbId, is_movie=False, season=metadata.season, + episode=metadata.episode) + else: + store_subtitles_movie(path_mappings.path_replace_reverse_movie(video_path), video_path) + event_stream(type='movie', payload=id) + + if settings.general.use_plex and settings.plex.update_movie_library: + plex_refresh_item(metadata.imdbId, is_movie=True) def subtitles_lang_from_filename(path): diff --git a/bazarr/api/subtitles/subtitles_contents.py b/bazarr/api/subtitles/subtitles_contents.py new file mode 100644 index 0000000000..81eff96c84 --- /dev/null +++ b/bazarr/api/subtitles/subtitles_contents.py @@ -0,0 +1,82 @@ +# coding=utf-8 +import logging +import srt + +from flask_restx import Resource, Namespace, reqparse, fields, marshal + +from ..utils import authenticate + + +api_ns_subtitle_contents = Namespace('Subtitle Contents', description='Retrieve contents of subtitle file') + + +@api_ns_subtitle_contents.route('subtitles/contents') +class SubtitleNameContents(Resource): + get_request_parser = reqparse.RequestParser() + get_request_parser.add_argument('subtitlePath', type=str, required=True, help='Subtitle filepath') + + time_modal = api_ns_subtitle_contents.model('time_modal', { + 'hours': fields.Integer(), + 'minutes': fields.Integer(), + 'seconds': fields.Integer(), + 'total_seconds': fields.Integer(), + 'microseconds': fields.Integer(), + }) + + get_response_model = api_ns_subtitle_contents.model('SubtitlesContentsGetResponse', { + 'index': fields.Integer(), + 'content': fields.String(), + 'proprietary': fields.String(), + 'start': fields.Nested(time_modal), + 'end': fields.Nested(time_modal), + # 'duration': fields.Nested(time_modal), + }) + + @authenticate + @api_ns_subtitle_contents.response(200, 'Success') + @api_ns_subtitle_contents.response(401, 'Not Authenticated') + @api_ns_subtitle_contents.doc(parser=get_request_parser) + def get(self): + """Retrieve subtitle file contents""" + + args = self.get_request_parser.parse_args() + path = args.get('subtitlePath') + + results = [] + + # Load the SRT content + with open(path, "r", encoding="utf-8") as f: + file_content = f.read() + + # Map contents + for sub in srt.parse(file_content): + + start_total_seconds = int(sub.start.total_seconds()) + end_total_seconds = int(sub.end.total_seconds()) + duration_timedelta = sub.end - sub.start + + results.append(dict( + index=sub.index, + content=sub.content, + proprietary=sub.proprietary, + start=dict( + hours = start_total_seconds // 3600, + minutes = (start_total_seconds % 3600) // 60, + seconds = start_total_seconds % 60, + total_seconds=int(sub.start.total_seconds()), + microseconds = sub.start.microseconds + ), + end=dict( + hours = end_total_seconds // 3600, + minutes = (end_total_seconds % 3600) // 60, + seconds = end_total_seconds % 60, + total_seconds=int(sub.end.total_seconds()), + microseconds = sub.end.microseconds + ), + # duration=dict( + # seconds=int(duration_timedelta.total_seconds()), + # microseconds=duration_timedelta.microseconds + # ), + )) + + return marshal(results, self.get_response_model, envelope='data') diff --git a/bazarr/api/system/account.py b/bazarr/api/system/account.py index 1f002c8a25..f8b22b30b4 100644 --- a/bazarr/api/system/account.py +++ b/bazarr/api/system/account.py @@ -1,21 +1,77 @@ # coding=utf-8 import gc +import time +import secrets +import logging +import threading +from collections import OrderedDict from flask import session, request from flask_restx import Resource, Namespace, reqparse from app.config import settings -from utilities.helper import check_credentials +from utilities.helper import check_credentials, needs_password_upgrade, upgrade_password_hash api_ns_system_account = Namespace('System Account', description='Login or logout from Bazarr UI') +# In-memory login rate limiter: {ip: (fail_count, last_fail_time)} +_login_attempts = OrderedDict() +_login_lock = threading.Lock() +_MAX_ATTEMPTS = 5 +_LOCKOUT_SECONDS = 300 # 5 minutes +_MAX_TRACKED_IPS = 10000 + + +def _get_client_ip(): + # Only use remote_addr by default to prevent IP spoofing via headers. + # Reverse proxies should be configured to set X-Real-IP reliably. + return request.remote_addr + + +def _is_rate_limited(ip): + with _login_lock: + if ip not in _login_attempts: + return False + count, last_time = _login_attempts[ip] + if time.time() - last_time > _LOCKOUT_SECONDS: + del _login_attempts[ip] + return False + return count >= _MAX_ATTEMPTS + + +def _record_failed_attempt(ip): + with _login_lock: + # Evict oldest entries if at capacity + while len(_login_attempts) >= _MAX_TRACKED_IPS: + _login_attempts.popitem(last=False) + + now = time.time() + if ip in _login_attempts: + count, last_time = _login_attempts[ip] + if now - last_time > _LOCKOUT_SECONDS: + _login_attempts[ip] = (1, now) + else: + _login_attempts[ip] = (count + 1, now) + else: + _login_attempts[ip] = (1, now) + + count = _login_attempts[ip][0] + if count >= _MAX_ATTEMPTS: + logging.warning(f'Login rate limit triggered for {ip} after {count} failed attempts') + + +def _clear_failed_attempts(ip): + with _login_lock: + _login_attempts.pop(ip, None) + @api_ns_system_account.hide @api_ns_system_account.route('system/account') class SystemAccount(Resource): post_request_parser = reqparse.RequestParser() - post_request_parser.add_argument('action', type=str, required=True, help='Action from ["login", "logout"]') + post_request_parser.add_argument('action', type=str, required=True, + help='Action from ["login", "logout", "upgrade_hash"]') post_request_parser.add_argument('username', type=str, required=False, help='Bazarr username') post_request_parser.add_argument('password', type=str, required=False, help='Bazarr password') @@ -24,6 +80,7 @@ class SystemAccount(Resource): @api_ns_system_account.response(400, 'Unknown action') @api_ns_system_account.response(403, 'Authentication failed') @api_ns_system_account.response(406, 'Browser must be closed to invalidate basic authentication') + @api_ns_system_account.response(429, 'Too many failed login attempts') @api_ns_system_account.response(500, 'Unknown authentication type define in config') def post(self): """Login or logout from Bazarr UI when using form login""" @@ -33,12 +90,23 @@ def post(self): action = args.get('action') if action == 'login': + ip = _get_client_ip() + if _is_rate_limited(ip): + return 'Too many failed login attempts. Try again in 5 minutes.', 429 + username = args.get('username') password = args.get('password') if check_credentials(username, password, request): + _clear_failed_attempts(ip) session['logged_in'] = True + if needs_password_upgrade(): + # Store password in session for upgrade (server-side only, never sent to client) + session['_pw_for_upgrade'] = password + session['_upgrade_token'] = secrets.token_urlsafe(16) + return {'upgrade_hash': True, 'upgrade_token': session['_upgrade_token']}, 200 return '', 204 else: + _record_failed_attempt(ip) session['logged_in'] = False return 'Authentication failed', 403 elif action == 'logout': @@ -48,5 +116,16 @@ def post(self): session.clear() gc.collect() return '', 204 + elif action == 'upgrade_hash': + # Verify upgrade token from session (no password re-transmission) + token = args.get('password') # reuse password field for token + stored_token = session.get('_upgrade_token') + stored_pw = session.get('_pw_for_upgrade') + if not stored_token or not stored_pw or token != stored_token: + return 'Invalid or expired upgrade token', 403 + upgrade_password_hash(stored_pw) + session.pop('_pw_for_upgrade', None) + session.pop('_upgrade_token', None) + return '', 204 return 'Unknown action', 400 diff --git a/bazarr/api/system/releases.py b/bazarr/api/system/releases.py index 12922dace1..867d795c55 100644 --- a/bazarr/api/system/releases.py +++ b/bazarr/api/system/releases.py @@ -18,11 +18,12 @@ @api_ns_system_releases.route('system/releases') class SystemReleases(Resource): get_response_model = api_ns_system_releases.model('SystemBackupsGetResponse', { - 'body': fields.List(fields.String), + 'body': fields.String(), 'name': fields.String(), 'date': fields.String(), 'prerelease': fields.Boolean(), 'current': fields.Boolean(), + 'repo': fields.String(), }) @authenticate @@ -49,12 +50,12 @@ def get(self): current_version = os.environ["BAZARR_VERSION"] for i, release in enumerate(filtered_releases): - body = release['body'].replace('- ', '').split('\n')[1:] - filtered_releases[i] = {"body": body, + filtered_releases[i] = {"body": release['body'] or '', "name": release['name'], "date": release['date'][:10], "prerelease": release['prerelease'], - "current": release['name'].lstrip('v') == current_version} + "current": release['name'].lstrip('v') == current_version, + "repo": release.get('repo', 'Bazarr+')} except Exception: logging.exception( diff --git a/bazarr/api/translator/translator.py b/bazarr/api/translator/translator.py index f75ca0b177..114b99f722 100644 --- a/bazarr/api/translator/translator.py +++ b/bazarr/api/translator/translator.py @@ -2,9 +2,12 @@ import logging import requests +from flask import request as flask_request from flask_restx import Resource, Namespace from app.config import settings +from app.jobs_queue import jobs_queue +from subtitles.tools.translate.services.auth import get_translator_auth_headers from ..utils import authenticate api_ns_translator = Namespace('Translator', description='AI Subtitle Translator service operations') @@ -33,11 +36,25 @@ def get(self): return {"error": "AI Subtitle Translator service URL not configured"}, 503 try: - response = requests.get(f"{service_url}/api/v1/status", timeout=10) + response = requests.get(f"{service_url}/api/v1/status", headers=get_translator_auth_headers(), timeout=10) if response.status_code == 200: - return response.json(), 200 + data = response.json() + # Count Bazarr-side pending translation jobs + pending_count = sum( + 1 for job in jobs_queue.jobs_pending_queue + if 'translat' in (job.job_name or '').lower() + ) + running_count = sum( + 1 for job in jobs_queue.jobs_running_queue + if 'translat' in (job.job_name or '').lower() + ) + data['bazarr_queue'] = { + 'pending': pending_count, + 'running': running_count, + } + return data, 200 else: - return {"error": f"Service returned {response.status_code}"}, response.status_code + return {"error": f"Service returned {response.status_code}"}, 502 except requests.exceptions.ConnectionError: return {"error": "Cannot connect to AI Subtitle Translator service"}, 503 except requests.exceptions.Timeout: @@ -60,11 +77,11 @@ def get(self): return {"error": "AI Subtitle Translator service URL not configured"}, 503 try: - response = requests.get(f"{service_url}/api/v1/jobs", timeout=10) + response = requests.get(f"{service_url}/api/v1/jobs", headers=get_translator_auth_headers(), timeout=10) if response.status_code == 200: return response.json(), 200 else: - return {"error": f"Service returned {response.status_code}"}, response.status_code + return {"error": f"Service returned {response.status_code}"}, 502 except requests.exceptions.ConnectionError: return {"error": "Cannot connect to AI Subtitle Translator service"}, 503 except requests.exceptions.Timeout: @@ -87,13 +104,13 @@ def get(self, job_id): return {"error": "AI Subtitle Translator service URL not configured"}, 503 try: - response = requests.get(f"{service_url}/api/v1/jobs/{job_id}", timeout=10) + response = requests.get(f"{service_url}/api/v1/jobs/{job_id}", headers=get_translator_auth_headers(), timeout=10) if response.status_code == 200: return response.json(), 200 elif response.status_code == 404: return {"error": "Job not found"}, 404 else: - return {"error": f"Service returned {response.status_code}"}, response.status_code + return {"error": f"Service returned {response.status_code}"}, 502 except requests.exceptions.ConnectionError: return {"error": "Cannot connect to AI Subtitle Translator service"}, 503 except Exception as e: @@ -111,13 +128,13 @@ def delete(self, job_id): return {"error": "AI Subtitle Translator service URL not configured"}, 503 try: - response = requests.delete(f"{service_url}/api/v1/jobs/{job_id}", timeout=10) + response = requests.delete(f"{service_url}/api/v1/jobs/{job_id}", headers=get_translator_auth_headers(), timeout=10) if response.status_code == 200: return response.json(), 200 elif response.status_code == 404: return {"error": "Job not found"}, 404 else: - return {"error": f"Service returned {response.status_code}"}, response.status_code + return {"error": f"Service returned {response.status_code}"}, 502 except requests.exceptions.ConnectionError: return {"error": "Cannot connect to AI Subtitle Translator service"}, 503 except Exception as e: @@ -138,11 +155,11 @@ def get(self): return {"error": "AI Subtitle Translator service URL not configured"}, 503 try: - response = requests.get(f"{service_url}/api/v1/models", timeout=10) + response = requests.get(f"{service_url}/api/v1/models", headers=get_translator_auth_headers(), timeout=10) if response.status_code == 200: return response.json(), 200 else: - return {"error": f"Service returned {response.status_code}"}, response.status_code + return {"error": f"Service returned {response.status_code}"}, 502 except requests.exceptions.ConnectionError: return {"error": "Cannot connect to AI Subtitle Translator service"}, 503 except requests.exceptions.Timeout: @@ -165,13 +182,70 @@ def get(self): return {"error": "AI Subtitle Translator service URL not configured"}, 503 try: - response = requests.get(f"{service_url}/api/v1/config", timeout=10) + response = requests.get(f"{service_url}/api/v1/config", headers=get_translator_auth_headers(), timeout=10) if response.status_code == 200: return response.json(), 200 else: - return {"error": f"Service returned {response.status_code}"}, response.status_code + return {"error": f"Service returned {response.status_code}"}, 502 except requests.exceptions.ConnectionError: return {"error": "Cannot connect to AI Subtitle Translator service"}, 503 except Exception as e: logger.error(f"Error getting config: {e}") + return {"error": str(e)}, 500 + + +@api_ns_translator.route('translator/test') +class TranslatorTest(Resource): + @authenticate + @api_ns_translator.doc( + responses={200: 'Success', 400: 'Bad Request', 503: 'Service Unavailable'} + ) + def post(self): + """Test connection, encryption, and API key with the translator service. + + Accepts optional JSON body with current (unsaved) form values: + - serviceUrl: override saved service URL + - apiKey: override saved OpenRouter API key + - encryptionKey: override saved encryption key + """ + data = flask_request.get_json(silent=True) or {} + + service_url = data.get("serviceUrl") or get_service_url() + if service_url: + service_url = service_url.rstrip("/") + if not service_url: + return {"error": "AI Subtitle Translator service URL not configured"}, 503 + + api_key = data.get("apiKey") or settings.translator.openrouter_api_key + if not api_key: + return {"error": "OpenRouter API key not configured"}, 400 + + encryption_key = data.get("encryptionKey") if "encryptionKey" in data else settings.translator.openrouter_encryption_key + if encryption_key: + try: + from subtitles.tools.translate.services.encryption import encrypt_api_key + api_key = encrypt_api_key(api_key, encryption_key) + except ValueError as e: + return {"error": f"Invalid encryption key: {e}"}, 400 + + try: + response = requests.post( + f"{service_url}/api/v1/test", + json={"apiKey": api_key}, + headers={"Content-Type": "application/json", **get_translator_auth_headers(encryption_key)}, + timeout=10 + ) + if response.status_code == 200: + return response.json(), 200 + else: + try: + return response.json(), 502 + except (ValueError, requests.exceptions.JSONDecodeError): + return {"error": f"Service returned {response.status_code}"}, 502 + except requests.exceptions.ConnectionError: + return {"error": "Cannot connect to AI Subtitle Translator service"}, 503 + except requests.exceptions.Timeout: + return {"error": "Service timeout"}, 503 + except Exception as e: + logger.error(f"Error testing translator: {e}") return {"error": str(e)}, 500 \ No newline at end of file diff --git a/bazarr/api/utils.py b/bazarr/api/utils.py index c8dac68bb4..8341c9b7ed 100644 --- a/bazarr/api/utils.py +++ b/bazarr/api/utils.py @@ -1,6 +1,8 @@ # coding=utf-8 import ast +import hmac +import logging from functools import wraps from flask import request, abort @@ -16,17 +18,32 @@ False_Keys = ['False', 'false', '0'] +def _safe_apikey_compare(provided, expected): + if not provided: + return False + return hmac.compare_digest(str(provided), str(expected)) + + def authenticate(actual_method): @wraps(actual_method) def wrapper(*args, **kwargs): apikey_settings = settings.auth.apikey + apikey_header = request.headers.get('X-API-KEY') + + if _safe_apikey_compare(apikey_header, apikey_settings): + return actual_method(*args, **kwargs) + + # Legacy: accept API key from query string or form data with deprecation warning + # Suppress warning for webhook endpoints (Plex webhooks use ?apikey= in callback URLs) apikey_get = request.args.get('apikey') apikey_post = request.form.get('apikey') - apikey_header = None - if 'X-API-KEY' in request.headers: - apikey_header = request.headers['X-API-KEY'] - - if apikey_settings in [apikey_get, apikey_post, apikey_header]: + if _safe_apikey_compare(apikey_get, apikey_settings) or _safe_apikey_compare(apikey_post, apikey_settings): + if '/webhooks/' not in request.path: + logging.warning( + 'API key passed via query string or form data is deprecated. ' + 'Use the X-API-KEY header instead. ' + 'Endpoint: %s %s', request.method, request.path + ) return actual_method(*args, **kwargs) return abort(401) diff --git a/bazarr/app/announcements.py b/bazarr/app/announcements.py index 753399dfa7..831f7d6da8 100644 --- a/bazarr/app/announcements.py +++ b/bazarr/app/announcements.py @@ -1,7 +1,6 @@ # coding=utf-8 import os -import sys import hashlib import requests import logging @@ -11,21 +10,12 @@ from datetime import datetime from operator import itemgetter -from app.get_providers import get_enabled_providers from app.database import TableAnnouncements, database, insert, select -from app.config import settings from app.get_args import args -from sonarr.info import get_sonarr_info -from radarr.info import get_radarr_info from app.jobs_queue import jobs_queue -def upcoming_deprecated_python_version(): - # return True if Python version is deprecated - return sys.version_info.major == 2 or (sys.version_info.major == 3 and sys.version_info.minor < 10) - - # Announcements as receive by browser must be in the form of a list of dicts converted to JSON # [ # { @@ -49,32 +39,33 @@ def parse_announcement_dict(announcement_dict): return announcement_dict -def get_announcements_to_file(job_id=None, startup=False): +def get_announcements_to_file(job_id=None, startup=False, wait_for_completion=False): if not startup and not job_id: - jobs_queue.add_job_from_function("Updating Announcements File", is_progress=False) + jobs_queue.add_job_from_function("Updating Announcements File", is_progress=False, + wait_for_completion=wait_for_completion) return try: r = requests.get( - url="https://cdn.jsdelivr.net/gh/morpheus65535/bazarr-binaries@latest/announcements.json", + url="https://cdn.jsdelivr.net/gh/LavX/bazarr-binaries@master/announcements.json", timeout=30 ) + r.raise_for_status() except Exception: try: logging.exception("Error trying to get announcements from jsdelivr.net, falling back to Github.") r = requests.get( - url="https://raw.githubusercontent.com/morpheus65535/bazarr-binaries/refs/heads/master/announcements.json", + url="https://raw.githubusercontent.com/LavX/bazarr-binaries/refs/heads/master/announcements.json", timeout=30 ) + r.raise_for_status() except Exception: logging.exception("Error trying to get announcements from Github.") return - else: - with open(os.path.join(args.config_dir, 'config', 'announcements.json'), 'wb') as f: - f.write(r.content) - finally: - if not startup: - jobs_queue.update_job_name(job_id=job_id, new_job_name="Updated Announcements File") + with open(os.path.join(args.config_dir, 'config', 'announcements.json'), 'wb') as f: + f.write(r.content) + if not startup: + jobs_queue.update_job_name(job_id=job_id, new_job_name="Updated Announcements File") def get_online_announcements(): @@ -94,65 +85,7 @@ def get_online_announcements(): def get_local_announcements(): - announcements = [] - - # opensubtitles.org end-of-life - enabled_providers = get_enabled_providers() - if enabled_providers and 'opensubtitles' in enabled_providers and not settings.opensubtitles.vip: - announcements.append({ - 'text': 'Opensubtitles.org is deprecated for non-VIP users, migrate to Opensubtitles.com ASAP and disable ' - 'this provider to remove this announcement.', - 'link': 'https://wiki.bazarr.media/Troubleshooting/OpenSubtitles-migration/', - 'dismissible': True, - 'timestamp': 1676236978, - }) - - # opensubtitles-scraper alternative announcement - if enabled_providers and 'opensubtitles' in enabled_providers: - announcements.append({ - 'text': 'An alternative OpenSubtitles scraper service is available that bypasses API limitations. ' - 'Consider using opensubtitles-scraper for improved subtitle downloads.', - 'link': 'https://github.com/morpheus65535/bazarr/tree/master/opensubtitles-scraper', - 'dismissible': True, - 'timestamp': 1765805148, - }) - - - # deprecated Sonarr and Radarr versions - if get_sonarr_info.is_deprecated(): - announcements.append({ - 'text': f'Sonarr {get_sonarr_info.version()} is deprecated and unsupported. You should consider upgrading ' - f'as Bazarr will eventually drop support for deprecated Sonarr version.', - 'link': 'https://forums.sonarr.tv/t/v3-is-now-officially-stable-v2-is-eol/27858', - 'dismissible': False, - 'timestamp': 1679606061, - }) - if get_radarr_info.is_deprecated(): - announcements.append({ - 'text': f'Radarr {get_radarr_info.version()} is deprecated and unsupported. You should consider upgrading ' - f'as Bazarr will eventually drop support for deprecated Radarr version.', - 'link': 'https://discord.com/channels/264387956343570434/264388019585286144/1051567458697363547', - 'dismissible': False, - 'timestamp': 1679606309, - }) - - # upcoming deprecated Python versions - if upcoming_deprecated_python_version(): - announcements.append({ - 'text': 'Starting with Bazarr 1.6, support for Python 3.8 and 3.9 will get dropped. Upgrade your current ' - 'version of Python ASAP to get further updates.', - 'link': 'https://wiki.bazarr.media/Troubleshooting/Windows_installer_reinstall/', - 'dismissible': False, - 'timestamp': 1744469706, - }) - - for announcement in announcements: - if 'enabled' not in announcement: - announcement['enabled'] = True - if 'dismissible' not in announcement: - announcement['dismissible'] = True - - return announcements + return [] def get_all_announcements(): diff --git a/bazarr/app/check_update.py b/bazarr/app/check_update.py index 3a6d4beadc..c02872bb71 100644 --- a/bazarr/app/check_update.py +++ b/bazarr/app/check_update.py @@ -17,63 +17,103 @@ # Fork configuration - allows overriding via environment variable # Default: LavX/bazarr (this fork) -# To use upstream releases, set BAZARR_UPSTREAM_REPO=morpheus65535/Bazarr +# To use upstream releases, set BAZARR_RELEASES_REPO to the upstream repo RELEASES_REPO = os.environ.get('BAZARR_RELEASES_REPO', 'LavX/bazarr') +# Microservice repositories to include in releases page +MICROSERVICE_REPOS = [ + ('LavX/opensubtitles-scraper', 'OpenSubtitles Scraper'), + ('LavX/ai-subtitle-translator', 'AI Subtitle Translator'), +] + def deprecated_python_version(): # return True if Python version is deprecated - return sys.version_info.major == 2 or (sys.version_info.major == 3 and sys.version_info.minor < 8) - + return sys.version_info.major == 2 or (sys.version_info.major == 3 and sys.version_info.minor < 10) -def check_releases(job_id=None, startup=False): - # startup is used to prevent trying to create a job before the jobs queue is initialized - if not startup and not job_id: - jobs_queue.add_job_from_function("Updating Release Info", is_progress=False) - return +def _fetch_repo_releases(repo, label=None): + """Fetch releases from a single GitHub repo. Returns list of release dicts.""" releases = [] - url_releases = f'https://api.github.com/repos/{RELEASES_REPO}/releases?per_page=100' + url = f'https://api.github.com/repos/{repo}/releases?per_page=100' try: - logging.debug(f'BAZARR getting releases from Github: {url_releases}') - r = requests.get(url_releases, allow_redirects=True, timeout=15) + logging.debug(f'BAZARR getting releases from Github: {url}') + r = requests.get(url, allow_redirects=True, timeout=15) r.raise_for_status() except requests.exceptions.HTTPError: - logging.exception("Error trying to get releases from Github. Http error.") + logging.exception(f"Error trying to get releases from Github ({repo}). Http error.") except requests.exceptions.ConnectionError: - logging.exception("Error trying to get releases from Github. Connection Error.") + logging.exception(f"Error trying to get releases from Github ({repo}). Connection Error.") except requests.exceptions.Timeout: - logging.exception("Error trying to get releases from Github. Timeout Error.") + logging.exception(f"Error trying to get releases from Github ({repo}). Timeout Error.") except requests.exceptions.RequestException: - logging.exception("Error trying to get releases from Github.") + logging.exception(f"Error trying to get releases from Github ({repo}).") else: - for release in r.json(): + try: + releases_data = r.json() + except ValueError: + logging.error(f"Error parsing JSON from Github releases response ({repo}). Skipping.") + return releases + for release in releases_data: download_link = None - for asset in release['assets']: - if asset['name'] == 'bazarr.zip': - download_link = asset['browser_download_url'] - if not download_link: - continue - releases.append({'name': release['name'], - 'body': release['body'], - 'date': release['published_at'], - 'prerelease': release['prerelease'], - 'download_link': download_link}) - with open(os.path.join(args.config_dir, 'config', 'releases.txt'), 'w') as f: - json.dump(releases, f) - logging.debug(f'BAZARR saved {len(r.json())} releases to releases.txt') - finally: - if not startup: - jobs_queue.update_job_name(job_id=job_id, new_job_name="Updated Release Info") - - -def check_if_new_update(): + for asset in release.get('assets', []): + download_link = asset['browser_download_url'] + break + entry = {'name': release['name'], + 'body': release['body'] or '', + 'date': release['published_at'], + 'prerelease': release['prerelease'], + 'download_link': download_link} + if label: + entry['repo'] = label + releases.append(entry) + logging.debug(f'BAZARR fetched {len(releases)} releases from {repo}') + return releases + + +def check_releases(job_id=None, startup=False, wait_for_completion=False): + # startup is used to prevent trying to create a job before the jobs queue is initialized + if not startup and not job_id: + jobs_queue.add_job_from_function("Updating Release Info", is_progress=False, + wait_for_completion=wait_for_completion) + return + + releases = _fetch_repo_releases(RELEASES_REPO, label='Bazarr+') + + for repo, label in MICROSERVICE_REPOS: + releases.extend(_fetch_repo_releases(repo, label=label)) + + releases.sort(key=lambda r: r['date'], reverse=True) + + with open(os.path.join(args.config_dir, 'config', 'releases.txt'), 'w') as f: + json.dump(releases, f) + logging.debug(f'BAZARR saved {len(releases)} releases to releases.txt') + + if job_id: + jobs_queue.update_job_name(job_id=job_id, new_job_name="Updated Release Info") + + +def check_if_new_update(startup=False, job_id=None, wait_for_completion=False): + if not startup and not job_id: + jobs_queue.add_job_from_function("Checking for Bazarr update", is_progress=False, + wait_for_completion=wait_for_completion) + return + + # Skip auto-update when running from source (no BAZARR_VERSION set) + bazarr_version = os.environ.get("BAZARR_VERSION", "") + if not bazarr_version: + logging.debug('BAZARR running from source, skipping auto-update') + check_releases(startup=True) + return + if settings.general.branch == 'master': use_prerelease = False elif settings.general.branch == 'development': use_prerelease = True else: logging.error(f'BAZARR unknown branch provided to updater: {settings.general.branch}') + if job_id: + jobs_queue.update_job_name(job_id=job_id, new_job_name="Failed to check for Bazarr update") return logging.debug(f'BAZARR updater is using {settings.general.branch} branch') @@ -128,9 +168,14 @@ def check_if_new_update(): logging.debug('BAZARR no release found') else: logging.debug('BAZARR --no_update have been used as an argument') + if job_id: + jobs_queue.update_job_name(job_id=job_id, new_job_name="Checked for Bazarr update") def download_release(url): + if not url: + logging.debug('BAZARR release has no download URL, skipping update') + return r = None update_dir = os.path.join(args.config_dir, 'update') try: @@ -139,7 +184,7 @@ def download_release(url): logging.debug(f'BAZARR unable to create update directory {update_dir}') else: logging.debug(f'BAZARR downloading release from Github: {url}') - r = requests.get(url, allow_redirects=True) + r = requests.get(url, allow_redirects=True, timeout=300) if r: try: with open(os.path.join(update_dir, 'bazarr.zip'), 'wb') as f: diff --git a/bazarr/app/config.py b/bazarr/app/config.py index 74acc0cae9..941c4b35cd 100644 --- a/bazarr/app/config.py +++ b/bazarr/app/config.py @@ -89,7 +89,7 @@ def check_parser_binary(value): Validator('general.port', must_exist=True, default=6767, is_type_of=int, gte=1, lte=65535), Validator('general.hostname', must_exist=True, default=platform.node(), is_type_of=str), Validator('general.base_url', must_exist=True, default='', is_type_of=str), - Validator('general.instance_name', must_exist=True, default='Bazarr', is_type_of=str, + Validator('general.instance_name', must_exist=True, default='Bazarr+', is_type_of=str, apply_default_on_none=True), Validator('general.path_mappings', must_exist=True, default=[], is_type_of=list), Validator('general.debug', must_exist=True, default=False, is_type_of=bool), @@ -97,7 +97,7 @@ def check_parser_binary(value): is_in=['master', 'development']), Validator('general.auto_update', must_exist=True, default=True, is_type_of=bool), Validator('general.single_language', must_exist=True, default=False, is_type_of=bool), - Validator('general.minimum_score', must_exist=True, default=90, is_type_of=int, gte=0, lte=100), + Validator('general.minimum_score', must_exist=True, default=80, is_type_of=int, gte=0, lte=100), Validator('general.use_scenename', must_exist=True, default=True, is_type_of=bool), Validator('general.use_postprocessing', must_exist=True, default=False, is_type_of=bool), Validator('general.postprocessing_cmd', must_exist=True, default='', is_type_of=str), @@ -140,6 +140,7 @@ def check_parser_binary(value): is_in=['3d', '1w', '2w', '3w', '4w']), Validator('general.enabled_providers', must_exist=True, default=[], is_type_of=list), Validator('general.provider_priorities', must_exist=True, default={}, is_type_of=dict), + Validator('general.use_provider_priority', must_exist=True, default=True, is_type_of=bool), Validator('general.enabled_integrations', must_exist=True, default=[], is_type_of=list), Validator('general.multithreading', must_exist=True, default=True, is_type_of=bool), Validator('general.chmod_enabled', must_exist=True, default=False, is_type_of=bool), @@ -198,8 +199,9 @@ def check_parser_binary(value): # translating section Validator('translator.default_score', must_exist=True, default=50, is_type_of=int, gte=0), - Validator('translator.gemini_key', must_exist=True, default='', is_type_of=str, cast=str), + Validator('translator.gemini_keys', must_exist=True, default=[], is_type_of=list), Validator('translator.gemini_model', must_exist=True, default='gemini-2.0-flash', is_type_of=str, cast=str), + Validator('translator.gemini_batch_size', must_exist=True, default=300, is_type_of=int, gte=1), Validator('translator.translator_info', must_exist=True, default=True, is_type_of=bool), Validator('translator.translator_type', must_exist=True, default='google_translate', is_type_of=str, cast=str), Validator('translator.lingarr_url', must_exist=True, default='http://lingarr:9876', is_type_of=str), @@ -210,6 +212,8 @@ def check_parser_binary(value): Validator('translator.openrouter_max_concurrent', must_exist=True, default=2, is_type_of=int, gte=1, lte=10), Validator('translator.openrouter_reasoning', must_exist=True, default='disabled', is_type_of=str, is_in=['disabled', 'low', 'medium', 'high']), + Validator('translator.openrouter_parallel_batches', must_exist=True, default=4, is_type_of=int, gte=1, lte=8), + Validator('translator.openrouter_encryption_key', must_exist=True, default='', is_type_of=str, cast=str), Validator('translator.lingarr_token', must_exist=True, default='', is_type_of=str, cast=str), # sonarr section @@ -235,6 +239,7 @@ def check_parser_binary(value): Validator('sonarr.defer_search_signalr', must_exist=True, default=False, is_type_of=bool), Validator('sonarr.sync_only_monitored_series', must_exist=True, default=False, is_type_of=bool), Validator('sonarr.sync_only_monitored_episodes', must_exist=True, default=False, is_type_of=bool), + Validator('sonarr.verify_ssl', must_exist=True, default=False, is_type_of=bool), # radarr section Validator('radarr.ip', must_exist=True, default='127.0.0.1', is_type_of=str), @@ -256,6 +261,7 @@ def check_parser_binary(value): Validator('radarr.use_ffprobe_cache', must_exist=True, default=True, is_type_of=bool), Validator('radarr.defer_search_signalr', must_exist=True, default=False, is_type_of=bool), Validator('radarr.sync_only_monitored_movies', must_exist=True, default=False, is_type_of=bool), + Validator('radarr.verify_ssl', must_exist=True, default=False, is_type_of=bool), # plex section Validator('plex.ip', must_exist=True, default='127.0.0.1', is_type_of=str), @@ -277,6 +283,7 @@ def check_parser_binary(value): Validator('plex.user_id', must_exist=True, default='', is_type_of=(int, str)), Validator('plex.auth_method', must_exist=True, default='apikey', is_type_of=str, is_in=['apikey', 'oauth']), Validator('plex.encryption_key', must_exist=True, default='', is_type_of=str), + Validator('plex.verify_ssl', must_exist=True, default=False, is_type_of=bool), Validator('plex.server_machine_id', must_exist=True, default='', is_type_of=str), Validator('plex.server_name', must_exist=True, default='', is_type_of=str), Validator('plex.server_url', must_exist=True, default='', is_type_of=str), @@ -305,9 +312,9 @@ def check_parser_binary(value): Validator('opensubtitles.ssl', must_exist=True, default=False, is_type_of=bool), Validator('opensubtitles.timeout', must_exist=True, default=15, is_type_of=int, gte=1), Validator('opensubtitles.skip_wrong_fps', must_exist=True, default=False, is_type_of=bool), - # Web scraper mode - can be enabled via OPENSUBTITLES_USE_WEB_SCRAPER=true environment variable + # Web scraper mode - always enabled (OpenSubtitles.org login no longer available) Validator('opensubtitles.use_web_scraper', must_exist=True, - default=os.environ.get('OPENSUBTITLES_USE_WEB_SCRAPER', 'false').lower() in ('true', '1', 'yes'), + default=True, is_type_of=bool), # Scraper URL - can be set via OPENSUBTITLES_SCRAPER_URL environment variable Validator('opensubtitles.scraper_service_url', must_exist=True, @@ -451,33 +458,6 @@ def check_parser_binary(value): Validator('subsync.max_offset_seconds', must_exist=True, default=60, is_type_of=int, is_in=[60, 120, 300, 600]), - # series_scores section - Validator('series_scores.hash', must_exist=True, default=359, is_type_of=int), - Validator('series_scores.series', must_exist=True, default=180, is_type_of=int), - Validator('series_scores.year', must_exist=True, default=90, is_type_of=int), - Validator('series_scores.season', must_exist=True, default=30, is_type_of=int), - Validator('series_scores.episode', must_exist=True, default=30, is_type_of=int), - Validator('series_scores.release_group', must_exist=True, default=14, is_type_of=int), - Validator('series_scores.source', must_exist=True, default=7, is_type_of=int), - Validator('series_scores.audio_codec', must_exist=True, default=3, is_type_of=int), - Validator('series_scores.resolution', must_exist=True, default=2, is_type_of=int), - Validator('series_scores.video_codec', must_exist=True, default=2, is_type_of=int), - Validator('series_scores.streaming_service', must_exist=True, default=1, is_type_of=int), - Validator('series_scores.hearing_impaired', must_exist=True, default=1, is_type_of=int), - - # movie_scores section - Validator('movie_scores.hash', must_exist=True, default=119, is_type_of=int), - Validator('movie_scores.title', must_exist=True, default=60, is_type_of=int), - Validator('movie_scores.year', must_exist=True, default=30, is_type_of=int), - Validator('movie_scores.release_group', must_exist=True, default=13, is_type_of=int), - Validator('movie_scores.source', must_exist=True, default=7, is_type_of=int), - Validator('movie_scores.audio_codec', must_exist=True, default=3, is_type_of=int), - Validator('movie_scores.resolution', must_exist=True, default=2, is_type_of=int), - Validator('movie_scores.video_codec', must_exist=True, default=2, is_type_of=int), - Validator('movie_scores.streaming_service', must_exist=True, default=1, is_type_of=int), - Validator('movie_scores.edition', must_exist=True, default=1, is_type_of=int), - Validator('movie_scores.hearing_impaired', must_exist=True, default=1, is_type_of=int), - # postgresql section Validator('postgresql.enabled', must_exist=True, default=False, is_type_of=bool), Validator('postgresql.host', must_exist=True, default='localhost', is_type_of=str), @@ -589,6 +569,7 @@ def write_config(): 'excluded_series_types', 'enabled_providers', 'enabled_integrations', + 'gemini_keys', 'path_mappings', 'path_mappings_movie', 'remove_profile_tags', @@ -627,6 +608,20 @@ def write_config(): settings.embeddedsubtitles.unknown_as_fallback = True settings.embeddedsubtitles.fallback_lang = 'en' del settings.embeddedsubtitles.unknown_as_english + +# delete custom scores sections since we don't use this anymore +if hasattr(settings, 'series_scores'): + settings.unset('SERIES_SCORES') +if hasattr(settings, 'movie_scores'): + settings.unset('MOVIE_SCORES') + +# backward compatibility: migrate gemini_key to gemini_keys +if hasattr(settings.translator, 'gemini_key'): + legacy_key = str(settings.translator.gemini_key).strip() + if legacy_key and not settings.translator.gemini_keys: + settings.translator.gemini_keys = [legacy_key] + del settings.translator.gemini_key + # save updated settings to file write_config() @@ -704,12 +699,15 @@ def save_settings(settings_items): if value in empty_values and value != '': value = None - # try to cast string as integer + # try to cast string as integer or float if isinstance(value, str) and settings_keys[-1] not in str_keys: try: value = int(value) except ValueError: - pass + try: + value = float(value) + except ValueError: + pass # Make sure empty language list are stored correctly if settings_keys[-1] in array_keys and value[0] in empty_values: @@ -752,7 +750,8 @@ def save_settings(settings_items): if key == 'settings-auth-password': if value != settings.auth.password and value is not None: - value = hashlib.md5(f"{value}".encode('utf-8')).hexdigest() + from utilities.helper import hash_password + value = hash_password(value) if key == 'settings-general-debug': configure_debug = True @@ -1017,9 +1016,15 @@ def configure_proxy_func(): os.environ['NO_PROXY'] = exclude -def get_scores(): - settings = get_settings() - return {"movie": settings["movie_scores"], "episode": settings["series_scores"]} +_SSL_VERIFY_SERVICES = frozenset({'sonarr', 'radarr', 'plex'}) + + +def get_ssl_verify(service): + """Return the verify parameter for requests calls to a service.""" + if service not in _SSL_VERIFY_SERVICES: + raise ValueError(f"Unknown service for SSL verify: {service}") + return settings.get(f'{service}.verify_ssl', False) + def sync_checker(subtitle): diff --git a/bazarr/app/database.py b/bazarr/app/database.py index 8504d3342c..79cad39fb0 100644 --- a/bazarr/app/database.py +++ b/bazarr/app/database.py @@ -87,6 +87,8 @@ def set_sqlite_pragma(dbapi_connection, connection_record): cursor = dbapi_connection.cursor() cursor.execute("PRAGMA foreign_keys=ON") + cursor.execute("PRAGMA journal_mode=WAL") + cursor.execute("PRAGMA busy_timeout=5000") cursor.close() session_factory = sessionmaker(bind=engine) @@ -188,6 +190,7 @@ class TableHistory(Base): language = mapped_column(Text) provider = mapped_column(Text) score = mapped_column(Integer) + score_out_of = mapped_column(Integer, nullable=True) sonarrEpisodeId = mapped_column(Integer, ForeignKey('table_episodes.sonarrEpisodeId', ondelete='CASCADE')) sonarrSeriesId = mapped_column(Integer, ForeignKey('table_shows.sonarrSeriesId', ondelete='CASCADE')) subs_id = mapped_column(Text) @@ -209,6 +212,7 @@ class TableHistoryMovie(Base): provider = mapped_column(Text) radarrId = mapped_column(Integer, ForeignKey('table_movies.radarrId', ondelete='CASCADE')) score = mapped_column(Integer) + score_out_of = mapped_column(Integer, nullable=True) subs_id = mapped_column(Text) subtitles_path = mapped_column(Text) timestamp = mapped_column(DateTime, nullable=False, default=datetime.now) diff --git a/bazarr/app/get_providers.py b/bazarr/app/get_providers.py index 6d7739b1e0..7fed65069f 100644 --- a/bazarr/app/get_providers.py +++ b/bazarr/app/get_providers.py @@ -1,8 +1,8 @@ # coding=utf-8 import os +import json import datetime -import pytz import logging import subliminal_patch import pretty @@ -12,6 +12,7 @@ import traceback import re +from zoneinfo import ZoneInfo from requests import ConnectionError from subzero.language import Language from subliminal_patch.exceptions import (TooManyRequests, APIThrottled, ParseResponseError, IPAddressBlocked, @@ -31,8 +32,7 @@ _TRACEBACK_RE = re.compile(r'File "(.*?providers[\\/].*?)", line (\d+)') -def time_until_midnight(timezone): - # type: (datetime.datetime) -> datetime.timedelta +def time_until_midnight(timezone) -> datetime.timedelta: """ Get timedelta until midnight. """ @@ -45,13 +45,13 @@ def time_until_midnight(timezone): # Titulky resets its download limits at the start of a new day from its perspective - the Europe/Prague timezone # Needs to convert to offset-naive dt def titulky_limit_reset_timedelta(): - return time_until_midnight(timezone=pytz.timezone('Europe/Prague')) + return time_until_midnight(timezone=ZoneInfo('Europe/Prague')) # LegendasDivx reset its searches limit at approximately midnight, Lisbon time, every day. We wait 1 more hours just # to be sure. def legendasdivx_limit_reset_timedelta(): - return time_until_midnight(timezone=pytz.timezone('Europe/Lisbon')) + datetime.timedelta(minutes=60) + return time_until_midnight(timezone=ZoneInfo('Europe/Lisbon')) + datetime.timedelta(minutes=60) VALID_THROTTLE_EXCEPTIONS = (TooManyRequests, DownloadLimitExceeded, ServiceUnavailable, APIThrottled, @@ -202,7 +202,7 @@ def get_providers(): else: logging.info("Using %s again after %s, (disabled because: %s)", provider, throttle_desc, reason) del tp[provider] - set_throttled_providers(str(tp)) + set_throttled_providers(tp) # if forced only is enabled: # fixme: Prepared for forced only implementation to remove providers with don't support forced only subtitles # for provider in providers_list: # if provider in PROVIDERS_FORCED_OFF: @@ -397,7 +397,7 @@ def _handle_mgb(name, exception, ids, language): if ids: if exception.media_type == "series": - if 'sonarrSeriesId' in ids and 'sonarrEpsiodeId' in ids: + if 'sonarrSeriesId' in ids and 'sonarrEpisodeId' in ids: blacklist_log(ids['sonarrSeriesId'], ids['sonarrEpisodeId'], name, exception.id, language_str) else: blacklist_log_movie(ids['radarrId'], name, exception.id, language_str) @@ -411,7 +411,7 @@ def provider_throttle(name, exception, ids=None, language=None): cls_name = getattr(cls, "__name__") if cls not in VALID_THROTTLE_EXCEPTIONS: for valid_cls in VALID_THROTTLE_EXCEPTIONS: - if isinstance(cls, valid_cls): + if issubclass(cls, valid_cls): cls = valid_cls throttle_data = provider_throttle_map().get(name, provider_throttle_map()["default"]).get(cls, None) or \ @@ -424,7 +424,7 @@ def provider_throttle(name, exception, ids=None, language=None): throttle_until = datetime.datetime.now() + throttle_delta - if cls_name not in VALID_COUNT_EXCEPTIONS or throttled_count(name): + if cls_name not in VALID_COUNT_EXCEPTIONS or throttled_count(name, exception): if cls_name == 'ValueError' and isinstance(exception.args, tuple) and len(exception.args) and exception.args[ 0].startswith('unsupported pickle protocol'): for fn in subliminal_cache_region.backend.all_filenames: @@ -434,7 +434,7 @@ def provider_throttle(name, exception, ids=None, language=None): logging.debug("Couldn't remove cache file: %s", os.path.basename(fn)) else: tp[name] = (cls_name, throttle_until, throttle_description) - set_throttled_providers(str(tp)) + set_throttled_providers(tp) trac_info = _get_traceback_info(exception) @@ -465,7 +465,7 @@ def _get_traceback_info(exc: Exception): return message + extra -def throttled_count(name): +def throttled_count(name, exception=None): global throttle_count if name in list(throttle_count.keys()): if 'count' in list(throttle_count[name].keys()): @@ -483,9 +483,15 @@ def throttled_count(name): return True if throttle_count[name]['time'] <= datetime.datetime.now(): throttle_count[name] = {"count": 1, "time": (datetime.datetime.now() + datetime.timedelta(seconds=120))} - logging.info("Provider %s throttle count %s of 5, waiting 5sec and trying again", name, - throttle_count[name]['count']) - time.sleep(5) + + # Respect Retry-After from the exception if available, otherwise default to 5s + wait_seconds = 5 + if exception and hasattr(exception, 'retry_after') and exception.retry_after: + wait_seconds = max(1, min(exception.retry_after, 30)) # floor at 1s, cap at 30s + + logging.info("Provider %s throttle count %s of 5, waiting %ds and trying again", name, + throttle_count[name]['count'], wait_seconds) + time.sleep(wait_seconds) return False @@ -496,7 +502,7 @@ def update_throttled_provider(): for provider in list(tp): if provider not in providers_list: del tp[provider] - set_throttled_providers(str(tp)) + set_throttled_providers(tp) reason, until, throttle_desc = tp.get(provider, (None, None, None)) @@ -507,7 +513,7 @@ def update_throttled_provider(): else: logging.info("Using %s again after %s, (disabled because: %s)", provider, throttle_desc, reason) del tp[provider] - set_throttled_providers(str(tp)) + set_throttled_providers(tp) reason, until, throttle_desc = tp.get(provider, (None, None, None)) @@ -516,7 +522,7 @@ def update_throttled_provider(): if now >= until: logging.info("Using %s again after %s, (disabled because: %s)", provider, throttle_desc, reason) del tp[provider] - set_throttled_providers(str(tp)) + set_throttled_providers(tp) event_stream(type='badges') @@ -538,7 +544,7 @@ def reset_throttled_providers(only_auth_or_conf_error=False): 'PaymentRequired']: continue del tp[provider] - set_throttled_providers(str(tp)) + set_throttled_providers(tp) update_throttled_provider() if only_auth_or_conf_error: logging.info('BAZARR throttled providers have been reset (only AuthenticationError, ConfigurationError and ' @@ -547,24 +553,50 @@ def reset_throttled_providers(only_auth_or_conf_error=False): logging.info('BAZARR throttled providers have been reset.') +def _throttled_providers_path(): + return os.path.normpath(os.path.join(args.config_dir, 'config', 'throttled_providers.dat')) + + def get_throttled_providers(): providers = {} + dat_path = _throttled_providers_path() try: - if os.path.exists(os.path.join(args.config_dir, 'config', 'throttled_providers.dat')): - with open(os.path.normpath(os.path.join(args.config_dir, 'config', 'throttled_providers.dat')), 'r') as \ - handle: - providers = eval(handle.read()) + if os.path.exists(dat_path): + with open(dat_path, 'r') as handle: + content = handle.read() + if not content.strip(): + return providers + raw = json.loads(content) + for name, val in raw.items(): + cls_name, until_str, description = val + until = datetime.datetime.fromisoformat(until_str) if until_str else None + providers[name] = (cls_name, until, description) + except (json.JSONDecodeError, KeyError, ValueError): + logging.info("Migrating throttled_providers.dat from legacy format to JSON. Throttle state reset.") + set_throttled_providers(providers) except Exception: - # set empty content in throttled_providers.dat - logging.error("Invalid content in throttled_providers.dat. Resetting") - set_throttled_providers(str(providers)) - finally: - return providers + logging.exception("Unexpected error reading throttled_providers.dat. Resetting.") + set_throttled_providers(providers) + return providers def set_throttled_providers(data): - with open(os.path.normpath(os.path.join(args.config_dir, 'config', 'throttled_providers.dat')), 'w+') as handle: - handle.write(data) + if not isinstance(data, dict): + raise TypeError(f"set_throttled_providers expects a dict, got {type(data).__name__}") + dat_path = _throttled_providers_path() + serializable = {} + for name, val in data.items(): + cls_name, throttle_until, description = val + serializable[name] = ( + cls_name, + throttle_until.isoformat() if throttle_until else None, + description + ) + json_data = json.dumps(serializable) + tmp_path = dat_path + '.tmp' + with open(tmp_path, 'w') as handle: + handle.write(json_data) + os.replace(tmp_path, dat_path) tp = get_throttled_providers() diff --git a/bazarr/app/jobs_queue.py b/bazarr/app/jobs_queue.py index ae613b4cac..362c8edece 100644 --- a/bazarr/app/jobs_queue.py +++ b/bazarr/app/jobs_queue.py @@ -4,6 +4,7 @@ import importlib import inspect import os +import time from time import sleep from datetime import datetime @@ -233,6 +234,14 @@ def update_job_name(self, job_id: int, new_job_name: str) -> bool: return True return False + def get_job_name(self, job_id: int) -> str: + """Get the current name of a job by its ID.""" + queues = self.jobs_pending_queue + self.jobs_running_queue + self.jobs_failed_queue + self.jobs_completed_queue + for job in queues: + if job.job_id == job_id: + return job.job_name + return "" + def get_job_returned_value(self, job_id: int): """ Fetches the returned value of a job from the queue provided its unique identifier. @@ -335,7 +344,8 @@ def update_job_progress_status(self, job_id: int, is_progress: bool = False) -> return True return False - def add_job_from_function(self, job_name: str, is_progress: bool, progress_max: int = 0) -> int: + def add_job_from_function(self, job_name: str, is_progress: bool, progress_max: int = 0, + wait_for_completion: bool = False) -> int: """ Adds a job to the pending queue using the details of the calling function. The job is then executed. @@ -345,6 +355,8 @@ def add_job_from_function(self, job_name: str, is_progress: bool, progress_max: :type is_progress: bool :param progress_max: Maximum progress value for the job, default is 0. :type progress_max: int + :param wait_for_completion: Flag indicating whether to wait for the job to complete before returning. + :type wait_for_completion: bool :return: ID of the added job. :rtype: int """ @@ -380,6 +392,11 @@ def add_job_from_function(self, job_name: str, is_progress: bool, progress_max: job_id = self.feed_jobs_pending_queue(job_name=job_name, module=parent_function_path, func=parent_function_name, kwargs=arguments, is_progress=is_progress, progress_max=progress_max) + if wait_for_completion: + time.sleep(1) + while jobs_queue.get_job_status(job_id) in ['queued', 'running']: + time.sleep(1) + return job_id def remove_job_from_pending_queue(self, job_id: int): @@ -500,8 +517,21 @@ def consume_jobs_pending_queue(self): try: if self.jobs_pending_queue: with self._queue_lock: - can_run_job = (len(self.jobs_running_queue) < settings.general.concurrent_jobs - and len(self.jobs_pending_queue) > 0) + next_job = self.jobs_pending_queue[0] if self.jobs_pending_queue else None + if next_job: + is_translation = 'translat' in (next_job.job_name or '').lower() + if is_translation: + # Translation jobs respect their own concurrency limit + running_translations = sum( + 1 for j in self.jobs_running_queue + if 'translat' in (j.job_name or '').lower() + ) + max_translations = settings.translator.openrouter_max_concurrent + can_run_job = running_translations < max_translations + else: + can_run_job = len(self.jobs_running_queue) < settings.general.concurrent_jobs + else: + can_run_job = False if can_run_job: job_thread = Thread(target=self._run_job) diff --git a/bazarr/app/logger.py b/bazarr/app/logger.py index a5b998da1d..13422b18e0 100644 --- a/bazarr/app/logger.py +++ b/bazarr/app/logger.py @@ -9,7 +9,6 @@ from logging.handlers import TimedRotatingFileHandler from utilities.central import get_log_file_path -from pytz_deprecation_shim import PytzUsageWarning from .config import settings @@ -129,8 +128,6 @@ def filter(self, record): def configure_logging(debug=False): warnings.simplefilter('ignore', category=ResourceWarning) - warnings.simplefilter('ignore', category=PytzUsageWarning) - # warnings.simplefilter('ignore', category=SAWarning) if debug: log_level = logging.DEBUG diff --git a/bazarr/app/notifier.py b/bazarr/app/notifier.py index f248dfcce0..6988c187a6 100644 --- a/bazarr/app/notifier.py +++ b/bazarr/app/notifier.py @@ -37,7 +37,8 @@ def update_notifier(): database.execute( insert(TableSettingsNotifier) - .values(notifiers_added)) + .values(notifiers_added) + .on_conflict_do_nothing()) def get_notifier_providers(): diff --git a/bazarr/app/scheduler.py b/bazarr/app/scheduler.py index 7326e02e26..763cc6d4f3 100644 --- a/bazarr/app/scheduler.py +++ b/bazarr/app/scheduler.py @@ -25,6 +25,7 @@ from subtitles.indexer.series import series_full_scan_subtitles from subtitles.wanted import wanted_search_missing_subtitles_series, wanted_search_missing_subtitles_movies from subtitles.upgrade import upgrade_subtitles +from subtitles.mass_operations import mass_batch_operation from utilities.cache import cache_maintenance from utilities.health import check_health from utilities.backup import backup_to_zip @@ -108,6 +109,7 @@ def update_configurable_tasks(self): self.__update_bazarr_task() self.__search_wanted_subtitles_task() self.__upgrade_subtitles_task() + self.__mass_sync_task() self.__randomize_interval_task() self.__automatic_backup() if args.no_tasks: @@ -202,22 +204,24 @@ def __sonarr_update_task(self): self.aps_scheduler.add_job( update_series, 'interval', minutes=int(settings.sonarr.series_sync), max_instances=1, coalesce=True, misfire_grace_time=15, id='update_series', name='Sync with Sonarr', - replace_existing=True) + replace_existing=True, kwargs=dict(wait_for_completion=True)) def __radarr_update_task(self): if settings.general.use_radarr: self.aps_scheduler.add_job( update_movies, 'interval', minutes=int(settings.radarr.movies_sync), max_instances=1, coalesce=True, misfire_grace_time=15, id='update_movies', name='Sync with Radarr', - replace_existing=True) + replace_existing=True, kwargs=dict(wait_for_completion=True)) def __cache_cleanup_task(self): self.aps_scheduler.add_job(cache_maintenance, 'interval', hours=24, max_instances=1, coalesce=True, - misfire_grace_time=15, id='cache_cleanup', name='Cache Maintenance') + misfire_grace_time=15, id='cache_cleanup', name='Cache Maintenance', + kwargs=dict(wait_for_completion=True)) def __check_health_task(self): self.aps_scheduler.add_job(check_health, 'interval', hours=6, max_instances=1, coalesce=True, - misfire_grace_time=15, id='check_health', name='Check Health') + misfire_grace_time=15, id='check_health', name='Check Health', + kwargs=dict(wait_for_completion=True)) def __automatic_backup(self): backup = settings.backup.frequency @@ -229,7 +233,8 @@ def __automatic_backup(self): trigger = {'year': in_a_century()} self.aps_scheduler.add_job(backup_to_zip, 'cron', **trigger, max_instances=1, coalesce=True, misfire_grace_time=15, id='backup', - name='Backup Database and Configuration File', replace_existing=True) + name='Backup Database and Configuration File', replace_existing=True, + kwargs=dict(wait_for_completion=True)) def __sonarr_full_update_task(self): if settings.general.use_sonarr: @@ -238,17 +243,18 @@ def __sonarr_full_update_task(self): self.aps_scheduler.add_job( series_full_scan_subtitles, 'cron', hour=settings.sonarr.full_update_hour, max_instances=1, coalesce=True, misfire_grace_time=15, id='series_full_scan_subtitles', - name='Index All Existing Episodes Subtitles', replace_existing=True) + name='Index All Existing Episodes Subtitles', replace_existing=True, + kwargs=dict(wait_for_completion=True)) elif full_update == "Weekly": self.aps_scheduler.add_job( series_full_scan_subtitles, 'cron', day_of_week=settings.sonarr.full_update_day, hour=settings.sonarr.full_update_hour, max_instances=1, coalesce=True, misfire_grace_time=15, id='series_full_scan_subtitles', name='Index All Existing Episodes Subtitles', - replace_existing=True) + replace_existing=True, kwargs=dict(wait_for_completion=True)) elif full_update == "Manually": self.aps_scheduler.add_job( series_full_scan_subtitles, 'cron', year=in_a_century(), max_instances=1, coalesce=True, - misfire_grace_time=15, id='series_full_scan_subtitles', + misfire_grace_time=15, id='series_full_scan_subtitles', kwargs=dict(wait_for_completion=True), name='Index All Existing Episodes Subtitles', replace_existing=True) def __radarr_full_update_task(self): @@ -257,65 +263,78 @@ def __radarr_full_update_task(self): if full_update == "Daily": self.aps_scheduler.add_job( movies_full_scan_subtitles, 'cron', hour=settings.radarr.full_update_hour, max_instances=1, - coalesce=True, misfire_grace_time=15, + coalesce=True, misfire_grace_time=15, kwargs=dict(wait_for_completion=True), id='movies_full_scan_subtitles', name='Index All Existing Movies Subtitles', replace_existing=True) elif full_update == "Weekly": self.aps_scheduler.add_job( movies_full_scan_subtitles, 'cron', day_of_week=settings.radarr.full_update_day, hour=settings.radarr.full_update_hour, max_instances=1, coalesce=True, misfire_grace_time=15, id='movies_full_scan_subtitles', - name='Index All Existing Movies Subtitles', replace_existing=True) + name='Index All Existing Movies Subtitles', replace_existing=True, + kwargs=dict(wait_for_completion=True)) elif full_update == "Manually": self.aps_scheduler.add_job( movies_full_scan_subtitles, 'cron', year=in_a_century(), max_instances=1, coalesce=True, misfire_grace_time=15, id='movies_full_scan_subtitles', name='Index All Existing Movies Subtitles', - replace_existing=True) + replace_existing=True, kwargs=dict(wait_for_completion=True)) def __update_bazarr_task(self): - if not args.no_update and os.environ["BAZARR_VERSION"] != '': + if not args.no_update and os.environ["BAZARR_VERSION"] not in ['', 'unknown']: task_name = 'Update Bazarr' if settings.general.auto_update: self.aps_scheduler.add_job( check_if_new_update, 'interval', hours=6, max_instances=1, coalesce=True, - misfire_grace_time=15, id='update_bazarr', name=task_name, replace_existing=True) + misfire_grace_time=15, id='update_bazarr', name=task_name, replace_existing=True, + kwargs=dict(wait_for_completion=True)) else: self.aps_scheduler.add_job( check_if_new_update, 'cron', year=in_a_century(), hour=4, id='update_bazarr', name=task_name, - replace_existing=True) + replace_existing=True, kwargs=dict(wait_for_completion=True)) self.aps_scheduler.add_job( check_releases, 'interval', hours=3, max_instances=1, coalesce=True, misfire_grace_time=15, - id='update_release', name='Update Release Info', replace_existing=True) + id='update_release', name='Update Release Info', replace_existing=True, + kwargs=dict(wait_for_completion=True)) else: self.aps_scheduler.add_job( check_releases, 'interval', hours=3, max_instances=1, coalesce=True, misfire_grace_time=15, - id='update_release', name='Update Release Info', replace_existing=True) + id='update_release', name='Update Release Info', replace_existing=True, + kwargs=dict(wait_for_completion=True)) self.aps_scheduler.add_job( get_announcements_to_file, 'interval', hours=6, max_instances=1, coalesce=True, misfire_grace_time=15, - id='update_announcements', name='Update Announcements File', replace_existing=True) + id='update_announcements', name='Update Announcements File', replace_existing=True, + kwargs=dict(wait_for_completion=True)) def __search_wanted_subtitles_task(self): if settings.general.use_sonarr: self.aps_scheduler.add_job( wanted_search_missing_subtitles_series, 'interval', hours=int(settings.general.wanted_search_frequency), max_instances=1, coalesce=True, misfire_grace_time=15, id='wanted_search_missing_subtitles_series', - replace_existing=True, name='Search for Missing Series Subtitles') + replace_existing=True, name='Search for Missing Series Subtitles', + kwargs=dict(wait_for_completion=True)) if settings.general.use_radarr: self.aps_scheduler.add_job( wanted_search_missing_subtitles_movies, 'interval', hours=int(settings.general.wanted_search_frequency_movie), max_instances=1, coalesce=True, misfire_grace_time=15, id='wanted_search_missing_subtitles_movies', - name='Search for Missing Movies Subtitles', replace_existing=True) + name='Search for Missing Movies Subtitles', replace_existing=True, + kwargs=dict(wait_for_completion=True)) def __upgrade_subtitles_task(self): if settings.general.use_sonarr or settings.general.use_radarr: self.aps_scheduler.add_job( upgrade_subtitles, 'interval', hours=int(settings.general.upgrade_frequency), max_instances=1, - coalesce=True, misfire_grace_time=15, id='upgrade_subtitles', + coalesce=True, misfire_grace_time=15, id='upgrade_subtitles', kwargs=dict(wait_for_completion=True), name='Upgrade Previously Downloaded Subtitles', replace_existing=True) + def __mass_sync_task(self): + self.aps_scheduler.add_job( + mass_batch_operation, 'cron', year=in_a_century(), max_instances=1, + coalesce=True, misfire_grace_time=15, id='mass_sync_subtitles', + name='Mass Sync All Subtitles', replace_existing=True) + def __randomize_interval_task(self): for job in self.aps_scheduler.get_jobs(): if isinstance(job.trigger, IntervalTrigger): diff --git a/bazarr/app/ui.py b/bazarr/app/ui.py index 09193837ab..77964c8f84 100644 --- a/bazarr/app/ui.py +++ b/bazarr/app/ui.py @@ -1,13 +1,15 @@ # coding=utf-8 import os +import ipaddress +import socket import requests import mimetypes from flask import (request, abort, render_template, Response, session, send_file, stream_with_context, Blueprint, redirect) from functools import wraps -from urllib.parse import unquote +from urllib.parse import unquote, urlparse from constants import HEADERS from literals import FILE_LOG @@ -16,7 +18,7 @@ from utilities.helper import check_credentials from utilities.central import get_log_file_path -from .config import settings, base_url +from .config import settings, base_url, get_ssl_verify from .database import database, System from .get_args import args @@ -132,7 +134,7 @@ def series_images(url): baseUrl = settings.sonarr.base_url url_image = f'{url_api_sonarr()}{url.lstrip(baseUrl)}?apikey={apikey}'.replace('poster-250', 'poster-500') try: - req = requests.get(url_image, stream=True, timeout=15, verify=False, headers=HEADERS) + req = requests.get(url_image, stream=True, timeout=15, verify=get_ssl_verify('sonarr'), headers=HEADERS) except Exception: return '', 404 else: @@ -146,7 +148,7 @@ def movies_images(url): baseUrl = settings.radarr.base_url url_image = f'{url_api_radarr()}{url.lstrip(baseUrl)}?apikey={apikey}' try: - req = requests.get(url_image, stream=True, timeout=15, verify=False, headers=HEADERS) + req = requests.get(url_image, stream=True, timeout=15, verify=get_ssl_verify('radarr'), headers=HEADERS) except Exception: return '', 404 else: @@ -175,6 +177,30 @@ def swaggerui_static(filename): return send_file(fullpath) +def _resolve_and_validate(url_str): + """Resolve DNS once and validate resolved IPs. Pick a safe address to pin to. + Returns (resolved_ip, hostname, parsed) or raises ValueError.""" + parsed = urlparse(url_str) + hostname = parsed.hostname + if not hostname: + raise ValueError("No hostname in URL") + port = parsed.port or (443 if parsed.scheme == 'https' else 80) + addrs = socket.getaddrinfo(hostname, port) + if not addrs: + raise ValueError("DNS resolution returned no results") + # Find a safe (non-link-local, non-loopback) address to pin to. + # Dual-stack hosts may resolve to both private LAN and link-local IPv6. + safe_ip = None + for _, _, _, _, sockaddr in addrs: + ip = ipaddress.ip_address(sockaddr[0]) + if not ip.is_link_local and not ip.is_loopback: + if safe_ip is None: + safe_ip = sockaddr[0] + if safe_ip is None: + raise ValueError("All resolved addresses are link-local or loopback") + return safe_ip, hostname, parsed + + @check_login @ui_bp.route('/test', methods=['GET']) @ui_bp.route('/test//', methods=['GET']) @@ -182,9 +208,19 @@ def proxy(protocol, url): if protocol.lower() not in ['http', 'https']: return dict(status=False, error='Unsupported protocol', code=0) url = f'{protocol}://{unquote(url)}' + try: + resolved_ip, hostname, parsed = _resolve_and_validate(url) + except (ValueError, socket.gaierror) as e: + return dict(status=False, error=f'Request blocked: {e}', code=0) + # Pin request to resolved IP to prevent DNS rebinding + port = parsed.port or (443 if parsed.scheme == 'https' else 80) + pinned_netloc = f'{resolved_ip}:{port}' + pinned_url = parsed._replace(netloc=pinned_netloc).geturl() + pinned_headers = dict(HEADERS) + pinned_headers['Host'] = hostname params = request.args try: - result = requests.get(url, params, allow_redirects=False, verify=False, timeout=5, headers=HEADERS) + result = requests.get(pinned_url, params, allow_redirects=False, verify=False, timeout=5, headers=pinned_headers) except Exception as e: return dict(status=False, error=repr(e)) else: diff --git a/bazarr/literals.py b/bazarr/literals.py index a76fa2ceec..09ba60d9b5 100644 --- a/bazarr/literals.py +++ b/bazarr/literals.py @@ -29,3 +29,4 @@ EXIT_PYTHON_UPGRADE_NEEDED = -103 EXIT_REQUIREMENTS_ERROR = -104 EXIT_PORT_ALREADY_IN_USE_ERROR = -105 +EXIT_UNEXPECTED_ERROR = -106 diff --git a/bazarr/main.py b/bazarr/main.py index aa2884aad7..61c8690568 100644 --- a/bazarr/main.py +++ b/bazarr/main.py @@ -38,7 +38,7 @@ else: # we want to update to the latest version before loading too much stuff. This should prevent deadlock when # there's missing embedded packages after a commit - check_if_new_update() + check_if_new_update(startup=True) from app.database import (System, database, update, migrate_db, create_db_revision, upgrade_languages_profile_values, fix_languages_profiles_with_duplicate_ids) # noqa E402 diff --git a/bazarr/radarr/filesystem.py b/bazarr/radarr/filesystem.py index 72a83cdb27..230665ea6e 100644 --- a/bazarr/radarr/filesystem.py +++ b/bazarr/radarr/filesystem.py @@ -3,7 +3,7 @@ import requests import logging -from app.config import settings +from app.config import settings, get_ssl_verify from radarr.info import url_api_radarr from constants import HEADERS @@ -15,7 +15,7 @@ def browse_radarr_filesystem(path='#'): url_radarr_api_filesystem = (f"{url_api_radarr()}filesystem?path={path}&allowFoldersWithoutTrailingSlashes=true&" f"includeFiles=false&apikey={settings.radarr.apikey}") try: - r = requests.get(url_radarr_api_filesystem, timeout=int(settings.radarr.http_timeout), verify=False, + r = requests.get(url_radarr_api_filesystem, timeout=int(settings.radarr.http_timeout), verify=get_ssl_verify('radarr'), headers=HEADERS) r.raise_for_status() except requests.exceptions.HTTPError: diff --git a/bazarr/radarr/history.py b/bazarr/radarr/history.py index d0cfe78c77..3898c35c5a 100644 --- a/bazarr/radarr/history.py +++ b/bazarr/radarr/history.py @@ -2,6 +2,8 @@ from datetime import datetime +from subliminal_patch.score import MAX_SCORES + from app.database import TableHistoryMovie, database, insert from app.event_handler import event_stream @@ -28,6 +30,7 @@ def history_log_movie(action, radarr_id, result, fake_provider=None, fake_score= language=language, provider=provider, score=score, + score_out_of=MAX_SCORES['movie'] if score else None, subs_id=subs_id, subtitles_path=subtitles_path, matched=str(matched) if matched else None, diff --git a/bazarr/radarr/info.py b/bazarr/radarr/info.py index ca6842d1ee..129be6803f 100644 --- a/bazarr/radarr/info.py +++ b/bazarr/radarr/info.py @@ -5,11 +5,11 @@ import datetime import semver -from requests.exceptions import JSONDecodeError +from requests.exceptions import JSONDecodeError, RequestException from dogpile.cache import make_region -from app.config import settings, empty_values +from app.config import settings, empty_values, get_ssl_verify from constants import HEADERS region = make_region().configure('dogpile.cache.memory') @@ -31,7 +31,7 @@ def version(): if settings.general.use_radarr: try: rv = f"{url_radarr()}/api/system/status?apikey={settings.radarr.apikey}" - radarr_json = requests.get(rv, timeout=int(settings.radarr.http_timeout), verify=False, + radarr_json = requests.get(rv, timeout=int(settings.radarr.http_timeout), verify=get_ssl_verify('radarr'), headers=HEADERS).json() if 'version' in radarr_json: radarr_version = radarr_json['version'] @@ -40,9 +40,9 @@ def version(): except JSONDecodeError: try: rv = f"{url_radarr()}/api/v3/system/status?apikey={settings.radarr.apikey}" - radarr_version = requests.get(rv, timeout=int(settings.radarr.http_timeout), verify=False, + radarr_version = requests.get(rv, timeout=int(settings.radarr.http_timeout), verify=get_ssl_verify('radarr'), headers=HEADERS).json()['version'] - except JSONDecodeError: + except (RequestException, JSONDecodeError, KeyError): logging.debug('BAZARR cannot get Radarr version') radarr_version = 'unknown' except Exception: diff --git a/bazarr/radarr/notify.py b/bazarr/radarr/notify.py index 3c74e1d5b8..35a6194977 100644 --- a/bazarr/radarr/notify.py +++ b/bazarr/radarr/notify.py @@ -3,7 +3,7 @@ import logging import requests -from app.config import settings +from app.config import settings, get_ssl_verify from radarr.info import url_api_radarr from constants import HEADERS @@ -15,6 +15,6 @@ def notify_radarr(radarr_id): 'name': 'RescanMovie', 'movieId': int(radarr_id) } - requests.post(url, json=data, timeout=int(settings.radarr.http_timeout), verify=False, headers=HEADERS) + requests.post(url, json=data, timeout=int(settings.radarr.http_timeout), verify=get_ssl_verify('radarr'), headers=HEADERS) except Exception: logging.exception('BAZARR cannot notify Radarr') diff --git a/bazarr/radarr/rootfolder.py b/bazarr/radarr/rootfolder.py index 1320cff8e3..24de040f61 100644 --- a/bazarr/radarr/rootfolder.py +++ b/bazarr/radarr/rootfolder.py @@ -4,7 +4,7 @@ import requests import logging -from app.config import settings +from app.config import settings, get_ssl_verify from utilities.path_mappings import path_mappings from app.database import TableMoviesRootfolder, TableMovies, database, delete, update, insert, select from radarr.info import url_api_radarr @@ -19,7 +19,7 @@ def get_radarr_rootfolder(): url_radarr_api_rootfolder = f"{url_api_radarr()}rootfolder?apikey={apikey_radarr}" try: - rootfolder = requests.get(url_radarr_api_rootfolder, timeout=int(settings.radarr.http_timeout), verify=False, headers=HEADERS) + rootfolder = requests.get(url_radarr_api_rootfolder, timeout=int(settings.radarr.http_timeout), verify=get_ssl_verify('radarr'), headers=HEADERS) except requests.exceptions.ConnectionError: logging.exception("BAZARR Error trying to get rootfolder from Radarr. Connection Error.") return [] diff --git a/bazarr/radarr/sync/movies.py b/bazarr/radarr/sync/movies.py index 3e9ae52373..f23c649c77 100644 --- a/bazarr/radarr/sync/movies.py +++ b/bazarr/radarr/sync/movies.py @@ -85,9 +85,10 @@ def add_movie(added_movie): event_stream(type='movie', action='update', payload=int(added_movie['radarrId'])) -def update_movies(job_id=None): +def update_movies(job_id=None, wait_for_completion=False): if not job_id: - jobs_queue.add_job_from_function("Syncing movies with Radarr", is_progress=True) + jobs_queue.add_job_from_function("Syncing movies with Radarr", is_progress=True, + wait_for_completion=wait_for_completion) return check_radarr_rootfolder() diff --git a/bazarr/radarr/sync/utils.py b/bazarr/radarr/sync/utils.py index d0d13c2531..eb21b47151 100644 --- a/bazarr/radarr/sync/utils.py +++ b/bazarr/radarr/sync/utils.py @@ -3,7 +3,7 @@ import requests import logging -from app.config import settings +from app.config import settings, get_ssl_verify from radarr.info import get_radarr_info, url_api_radarr from constants import HEADERS @@ -16,7 +16,7 @@ def get_profile_list(): f"apikey={apikey_radarr}") try: - profiles_json = requests.get(url_radarr_api_movies, timeout=int(settings.radarr.http_timeout), verify=False, + profiles_json = requests.get(url_radarr_api_movies, timeout=int(settings.radarr.http_timeout), verify=get_ssl_verify('radarr'), headers=HEADERS) except requests.exceptions.ConnectionError: logging.exception("BAZARR Error trying to get profiles from Radarr. Connection Error.") @@ -46,7 +46,7 @@ def get_tags(): url_radarr_api_series = f"{url_api_radarr()}tag?apikey={apikey_radarr}" try: - tagsDict = requests.get(url_radarr_api_series, timeout=int(settings.radarr.http_timeout), verify=False, headers=HEADERS) + tagsDict = requests.get(url_radarr_api_series, timeout=int(settings.radarr.http_timeout), verify=get_ssl_verify('radarr'), headers=HEADERS) except requests.exceptions.ConnectionError: logging.exception("BAZARR Error trying to get tags from Radarr. Connection Error.") return [] @@ -70,7 +70,7 @@ def get_movies_from_radarr_api(apikey_radarr, radarr_id=None): url_radarr_api_movies = f'{url_api_radarr()}movie{f"/{radarr_id}" if radarr_id else ""}?apikey={apikey_radarr}' try: - r = requests.get(url_radarr_api_movies, timeout=int(settings.radarr.http_timeout), verify=False, headers=HEADERS) + r = requests.get(url_radarr_api_movies, timeout=int(settings.radarr.http_timeout), verify=get_ssl_verify('radarr'), headers=HEADERS) if r.status_code == 404: return r.raise_for_status() @@ -100,7 +100,7 @@ def get_history_from_radarr_api(apikey_radarr, movie_id): url_radarr_api_history = f"{url_api_radarr()}history?eventType=1&movieIds={movie_id}&apikey={apikey_radarr}" try: - r = requests.get(url_radarr_api_history, timeout=int(settings.sonarr.http_timeout), verify=False, + r = requests.get(url_radarr_api_history, timeout=int(settings.sonarr.http_timeout), verify=get_ssl_verify('radarr'), headers=HEADERS) r.raise_for_status() except requests.exceptions.HTTPError: diff --git a/bazarr/sonarr/filesystem.py b/bazarr/sonarr/filesystem.py index 58790c7009..4d9f4c4c53 100644 --- a/bazarr/sonarr/filesystem.py +++ b/bazarr/sonarr/filesystem.py @@ -3,7 +3,7 @@ import requests import logging -from app.config import settings +from app.config import settings, get_ssl_verify from sonarr.info import url_api_sonarr from constants import HEADERS @@ -14,7 +14,7 @@ def browse_sonarr_filesystem(path='#'): url_sonarr_api_filesystem = (f"{url_api_sonarr()}filesystem?path={path}&allowFoldersWithoutTrailingSlashes=true&" f"includeFiles=false&apikey={settings.sonarr.apikey}") try: - r = requests.get(url_sonarr_api_filesystem, timeout=int(settings.sonarr.http_timeout), verify=False, + r = requests.get(url_sonarr_api_filesystem, timeout=int(settings.sonarr.http_timeout), verify=get_ssl_verify('sonarr'), headers=HEADERS) r.raise_for_status() except requests.exceptions.HTTPError: diff --git a/bazarr/sonarr/history.py b/bazarr/sonarr/history.py index c3f285aabb..fc1e513a84 100644 --- a/bazarr/sonarr/history.py +++ b/bazarr/sonarr/history.py @@ -2,6 +2,8 @@ from datetime import datetime +from subliminal_patch.score import MAX_SCORES + from app.database import TableHistory, database, insert from app.event_handler import event_stream @@ -30,6 +32,7 @@ def history_log(action, sonarr_series_id, sonarr_episode_id, result, fake_provid language=language, provider=provider, score=score, + score_out_of=MAX_SCORES['episode'] if score else None, subs_id=subs_id, subtitles_path=subtitles_path, matched=str(matched) if matched else None, diff --git a/bazarr/sonarr/info.py b/bazarr/sonarr/info.py index 6680789855..31508195ec 100644 --- a/bazarr/sonarr/info.py +++ b/bazarr/sonarr/info.py @@ -5,11 +5,11 @@ import datetime import semver -from requests.exceptions import JSONDecodeError +from requests.exceptions import JSONDecodeError, RequestException from dogpile.cache import make_region -from app.config import settings, empty_values +from app.config import settings, empty_values, get_ssl_verify from constants import HEADERS region = make_region().configure('dogpile.cache.memory') @@ -31,7 +31,7 @@ def version(): if settings.general.use_sonarr: try: sv = f"{url_sonarr()}/api/system/status?apikey={settings.sonarr.apikey}" - sonarr_json = requests.get(sv, timeout=int(settings.sonarr.http_timeout), verify=False, + sonarr_json = requests.get(sv, timeout=int(settings.sonarr.http_timeout), verify=get_ssl_verify('sonarr'), headers=HEADERS).json() if 'version' in sonarr_json: sonarr_version = sonarr_json['version'] @@ -40,9 +40,9 @@ def version(): except JSONDecodeError: try: sv = f"{url_sonarr()}/api/v3/system/status?apikey={settings.sonarr.apikey}" - sonarr_version = requests.get(sv, timeout=int(settings.sonarr.http_timeout), verify=False, + sonarr_version = requests.get(sv, timeout=int(settings.sonarr.http_timeout), verify=get_ssl_verify('sonarr'), headers=HEADERS).json()['version'] - except JSONDecodeError: + except (RequestException, JSONDecodeError, KeyError): logging.debug('BAZARR cannot get Sonarr version') sonarr_version = 'unknown' except Exception: diff --git a/bazarr/sonarr/notify.py b/bazarr/sonarr/notify.py index ee6207844e..ca0e8f56c9 100644 --- a/bazarr/sonarr/notify.py +++ b/bazarr/sonarr/notify.py @@ -3,7 +3,7 @@ import logging import requests -from app.config import settings +from app.config import settings, get_ssl_verify from sonarr.info import url_api_sonarr from constants import HEADERS @@ -15,6 +15,6 @@ def notify_sonarr(sonarr_series_id): 'name': 'RescanSeries', 'seriesId': int(sonarr_series_id) } - requests.post(url, json=data, timeout=int(settings.sonarr.http_timeout), verify=False, headers=HEADERS) + requests.post(url, json=data, timeout=int(settings.sonarr.http_timeout), verify=get_ssl_verify('sonarr'), headers=HEADERS) except Exception: logging.exception('BAZARR cannot notify Sonarr') diff --git a/bazarr/sonarr/rootfolder.py b/bazarr/sonarr/rootfolder.py index 46677a1e5b..f72c607e60 100644 --- a/bazarr/sonarr/rootfolder.py +++ b/bazarr/sonarr/rootfolder.py @@ -4,7 +4,7 @@ import requests import logging -from app.config import settings +from app.config import settings, get_ssl_verify from app.database import TableShowsRootfolder, TableShows, database, insert, update, delete, select from utilities.path_mappings import path_mappings from sonarr.info import url_api_sonarr @@ -19,7 +19,7 @@ def get_sonarr_rootfolder(): url_sonarr_api_rootfolder = f"{url_api_sonarr()}rootfolder?apikey={apikey_sonarr}" try: - rootfolder = requests.get(url_sonarr_api_rootfolder, timeout=int(settings.sonarr.http_timeout), verify=False, headers=HEADERS) + rootfolder = requests.get(url_sonarr_api_rootfolder, timeout=int(settings.sonarr.http_timeout), verify=get_ssl_verify('sonarr'), headers=HEADERS) except requests.exceptions.ConnectionError: logging.exception("BAZARR Error trying to get rootfolder from Sonarr. Connection Error.") return [] diff --git a/bazarr/sonarr/sync/series.py b/bazarr/sonarr/sync/series.py index fc9b451bef..8ff2976e2a 100644 --- a/bazarr/sonarr/sync/series.py +++ b/bazarr/sonarr/sync/series.py @@ -1,6 +1,7 @@ # coding=utf-8 import logging +import gc from sqlalchemy.exc import IntegrityError from datetime import datetime @@ -41,39 +42,20 @@ def get_series_monitored_table(): return series_dict -def update_series(job_id=None): +def update_series(job_id=None, wait_for_completion=False): if not job_id: - jobs_queue.add_job_from_function("Syncing series with Sonarr", is_progress=True) + jobs_queue.add_job_from_function("Syncing series with Sonarr", is_progress=True, + wait_for_completion=wait_for_completion) return + # Update root folders and update their health status check_sonarr_rootfolder() - apikey_sonarr = settings.sonarr.apikey - if apikey_sonarr is None: - return - - serie_default_enabled = settings.general.serie_default_enabled - - if serie_default_enabled is True: - serie_default_profile = settings.general.serie_default_profile - if serie_default_profile == '': - serie_default_profile = None - else: - serie_default_profile = None - - # Prevent trying to insert a series with a non-existing languages profileId - if (serie_default_profile and not database.execute( - select(TableLanguagesProfiles) - .where(TableLanguagesProfiles.profileId == serie_default_profile)) - .first()): - serie_default_profile = None - - audio_profiles = get_profile_list() - tagsDict = get_tags() - language_profiles = get_language_profiles() # Get shows data from Sonarr - series = get_series_from_sonarr_api(apikey_sonarr=apikey_sonarr) - if not isinstance(series, list): + try: + series = get_series_from_sonarr_api(apikey_sonarr=settings.sonarr.apikey) + except Exception as e: + logging.exception(f"BAZARR Error trying to get series from Sonarr: {e}") return else: # Get current shows in DB @@ -81,19 +63,24 @@ def update_series(job_id=None): database.execute( select(TableShows.sonarrSeriesId)) .all()] + current_shows_sonarr = [] series_count = len(series) - sync_monitored = settings.sonarr.sync_only_monitored_series - if sync_monitored: + skipped_count = 0 + + series_monitored = None + if settings.sonarr.sync_only_monitored_series: + # Get current series monitored status in DB series_monitored = get_series_monitored_table() - skipped_count = 0 + trace(f"Starting sync for {series_count} shows") jobs_queue.update_job_progress(job_id=job_id, progress_max=series_count) for i, show in enumerate(series, start=1): jobs_queue.update_job_progress(job_id=job_id, progress_value=i, progress_message=show['title']) - if sync_monitored: + + if settings.sonarr.sync_only_monitored_series: try: monitored_status_db = bool_map[series_monitored[show['id']]] except KeyError: @@ -113,71 +100,32 @@ def update_series(job_id=None): continue trace(f"{i}: (Processing) {show['title']}") + # Add shows in Sonarr to current shows list current_shows_sonarr.append(show['id']) - if show['id'] in current_shows_db: - updated_series = seriesParser(show, action='update', tags_dict=tagsDict, - language_profiles=language_profiles, - serie_default_profile=serie_default_profile, - audio_profiles=audio_profiles) - - if not database.execute( - select(TableShows) - .filter_by(**updated_series))\ - .first(): - try: - trace(f"Updating {show['title']}") - updated_series['updated_at_timestamp'] = datetime.now() - database.execute( - update(TableShows) - .values(updated_series) - .where(TableShows.sonarrSeriesId == show['id'])) - except IntegrityError as e: - logging.error(f"BAZARR cannot update series {updated_series['path']} because of {e}") - continue - - event_stream(type='series', payload=show['id']) - else: - added_series = seriesParser(show, action='insert', tags_dict=tagsDict, - language_profiles=language_profiles, - serie_default_profile=serie_default_profile, - audio_profiles=audio_profiles) - - try: - trace(f"Inserting {show['title']}") - added_series['created_at_timestamp'] = datetime.now() - database.execute( - insert(TableShows) - .values(added_series)) - except IntegrityError as e: - logging.error(f"BAZARR cannot insert series {added_series['path']} because of {e}") - continue - else: - list_missing_subtitles(no=show['id']) - - event_stream(type='series', action='update', payload=show['id']) + # Update series in DB + update_one_series(show['id'], action='updated') + # Update episodes in DB sync_episodes(series_id=show['id']) - # Remove old series from DB + # Calculate series to remove from DB removed_series = list(set(current_shows_db) - set(current_shows_sonarr)) for series in removed_series: - # try to avoid unnecessary database calls - if settings.general.debug: - series_title = database.execute(select(TableShows.title).where(TableShows.sonarrSeriesId == series)).first()[0] - trace(f"Deleting {series_title}") - database.execute( - delete(TableShows) - .where(TableShows.sonarrSeriesId == series)) - event_stream(type='series', action='delete', payload=series) - - if sync_monitored: - trace(f"skipped {skipped_count} unmonitored series out of {i}") + # Remove series from DB + update_one_series(series, action='deleted') + + if settings.sonarr.sync_only_monitored_series: + trace(f"skipped {skipped_count} unmonitored series out of {series_count}") + logging.debug('BAZARR All series synced from Sonarr into database.') + jobs_queue.update_job_name(job_id=job_id, new_job_name="Synced series with Sonarr") + gc.collect() + def update_one_series(series_id, action, is_signalr=False): logging.debug(f'BAZARR syncing this specific series from Sonarr: {series_id}') @@ -197,9 +145,7 @@ def update_one_series(series_id, action, is_signalr=False): event_stream(type='series', action='delete', payload=int(series_id)) return - serie_default_enabled = settings.general.serie_default_enabled - - if serie_default_enabled is True: + if settings.general.serie_default_enabled is True: serie_default_profile = settings.general.serie_default_profile if serie_default_profile == '': serie_default_profile = None @@ -209,58 +155,56 @@ def update_one_series(series_id, action, is_signalr=False): audio_profiles = get_profile_list() tagsDict = get_tags() language_profiles = get_language_profiles() + try: # Get series data from sonarr api - series = None - series_data = get_series_from_sonarr_api(apikey_sonarr=settings.sonarr.apikey, sonarr_series_id=int(series_id)) - + except Exception: + logging.exception(f'BAZARR cannot get series with ID {series_id} from Sonarr API.') + return + else: if not series_data: return else: if action == 'updated' and existing_series: + # Update existing series in DB series = seriesParser(series_data[0], action='update', tags_dict=tagsDict, language_profiles=language_profiles, serie_default_profile=serie_default_profile, audio_profiles=audio_profiles) + try: + series['updated_at_timestamp'] = datetime.now() + database.execute( + update(TableShows) + .values(series) + .where(TableShows.sonarrSeriesId == series['sonarrSeriesId'])) + except IntegrityError as e: + logging.error(f"BAZARR cannot update series {series['path']} because of {e}") + else: + if not is_signalr: + # Sonarr emit two SignalR events when episodes must be refreshed. + # The one that gets there doesn't include the episodeChanged flag. + # The episodes are synced only when this function is called from the + # frontend sync button in the episodes' page. + sync_episodes(series_id=int(series_id)) + event_stream(type='series', action='update', payload=int(series_id)) + logging.debug( + f'BAZARR updated this series into the database:{path_mappings.path_replace(series["path"])}') elif action == 'updated' and not existing_series: + # Insert new series in DB series = seriesParser(series_data[0], action='insert', tags_dict=tagsDict, language_profiles=language_profiles, serie_default_profile=serie_default_profile, audio_profiles=audio_profiles) - except Exception: - logging.exception('BAZARR cannot get series returned by SignalR feed from Sonarr API.') - return - # Update existing series in DB - if action == 'updated' and existing_series: - try: - series['updated_at_timestamp'] = datetime.now() - database.execute( - update(TableShows) - .values(series) - .where(TableShows.sonarrSeriesId == series['sonarrSeriesId'])) - except IntegrityError as e: - logging.error(f"BAZARR cannot update series {series['path']} because of {e}") - else: - if not is_signalr: - # Sonarr emit two SignalR events when episodes must be refreshed. - # The one that gets there doesn't include the episodeChanged flag. - # The episodes are synced only when this function is called from the - # frontend sync button in the episodes' page. - sync_episodes(series_id=int(series_id)) - event_stream(type='series', action='update', payload=int(series_id)) - logging.debug(f'BAZARR updated this series into the database:{path_mappings.path_replace(series["path"])}') - - # Insert new series in DB - elif action == 'updated' and not existing_series: - try: - series['created_at_timestamp'] = datetime.now() - database.execute( - insert(TableShows) - .values(series)) - except IntegrityError as e: - logging.error(f"BAZARR cannot insert series {series['path']} because of {e}") - else: - event_stream(type='series', action='update', payload=int(series_id)) - logging.debug(f'BAZARR inserted this series into the database:{path_mappings.path_replace(series["path"])}') + try: + series['created_at_timestamp'] = datetime.now() + database.execute( + insert(TableShows) + .values(series)) + except IntegrityError as e: + logging.error(f"BAZARR cannot insert series {series['path']} because of {e}") + else: + event_stream(type='series', action='update', payload=int(series_id)) + logging.debug( + f'BAZARR inserted this series into the database:{path_mappings.path_replace(series["path"])}') diff --git a/bazarr/sonarr/sync/utils.py b/bazarr/sonarr/sync/utils.py index 911fca6902..18301c391a 100644 --- a/bazarr/sonarr/sync/utils.py +++ b/bazarr/sonarr/sync/utils.py @@ -3,7 +3,7 @@ import requests import logging -from app.config import settings +from app.config import settings, get_ssl_verify from sonarr.info import get_sonarr_info, url_api_sonarr from constants import HEADERS @@ -22,7 +22,7 @@ def get_profile_list(): url_sonarr_api_series = f"{url_api_sonarr()}languageprofile?apikey={apikey_sonarr}" try: - profiles_json = requests.get(url_sonarr_api_series, timeout=int(settings.sonarr.http_timeout), verify=False, + profiles_json = requests.get(url_sonarr_api_series, timeout=int(settings.sonarr.http_timeout), verify=get_ssl_verify('sonarr'), headers=HEADERS) except requests.exceptions.ConnectionError: logging.exception("BAZARR Error trying to get profiles from Sonarr. Connection Error.") @@ -55,7 +55,7 @@ def get_tags(): url_sonarr_api_series = f"{url_api_sonarr()}tag?apikey={apikey_sonarr}" try: - tagsDict = requests.get(url_sonarr_api_series, timeout=int(settings.sonarr.http_timeout), verify=False, headers=HEADERS) + tagsDict = requests.get(url_sonarr_api_series, timeout=int(settings.sonarr.http_timeout), verify=get_ssl_verify('sonarr'), headers=HEADERS) except requests.exceptions.ConnectionError: logging.exception("BAZARR Error trying to get tags from Sonarr. Connection Error.") return [] @@ -73,7 +73,7 @@ def get_series_from_sonarr_api(apikey_sonarr, sonarr_series_id=None): url_sonarr_api_series = (f"{url_api_sonarr()}series/{sonarr_series_id if sonarr_series_id else ''}?" f"apikey={apikey_sonarr}") try: - r = requests.get(url_sonarr_api_series, timeout=int(settings.sonarr.http_timeout), verify=False, headers=HEADERS) + r = requests.get(url_sonarr_api_series, timeout=int(settings.sonarr.http_timeout), verify=get_ssl_verify('sonarr'), headers=HEADERS) r.raise_for_status() except requests.exceptions.HTTPError as e: if e.response.status_code: @@ -113,7 +113,7 @@ def get_episodes_from_sonarr_api(apikey_sonarr, series_id=None, episode_id=None) return try: - r = requests.get(url_sonarr_api_episode, timeout=int(settings.sonarr.http_timeout), verify=False, headers=HEADERS) + r = requests.get(url_sonarr_api_episode, timeout=int(settings.sonarr.http_timeout), verify=get_ssl_verify('sonarr'), headers=HEADERS) r.raise_for_status() except requests.exceptions.HTTPError: logging.exception("BAZARR Error trying to get episodes from Sonarr. Http error.") @@ -146,7 +146,7 @@ def get_episodesFiles_from_sonarr_api(apikey_sonarr, series_id=None, episode_fil return try: - r = requests.get(url_sonarr_api_episodeFiles, timeout=int(settings.sonarr.http_timeout), verify=False, + r = requests.get(url_sonarr_api_episodeFiles, timeout=int(settings.sonarr.http_timeout), verify=get_ssl_verify('sonarr'), headers=HEADERS) r.raise_for_status() except requests.exceptions.HTTPError: @@ -175,7 +175,7 @@ def get_history_from_sonarr_api(apikey_sonarr, episode_id): url_sonarr_api_history = f"{url_api_sonarr()}history?eventType=1&episodeId={episode_id}&apikey={apikey_sonarr}" try: - r = requests.get(url_sonarr_api_history, timeout=int(settings.sonarr.http_timeout), verify=False, + r = requests.get(url_sonarr_api_history, timeout=int(settings.sonarr.http_timeout), verify=get_ssl_verify('sonarr'), headers=HEADERS) r.raise_for_status() except requests.exceptions.HTTPError: diff --git a/bazarr/subtitles/cache.py b/bazarr/subtitles/cache.py new file mode 100644 index 0000000000..5abe50b300 --- /dev/null +++ b/bazarr/subtitles/cache.py @@ -0,0 +1,44 @@ +# coding=utf-8 + +import time +import threading +import uuid + + +_TTL_SECONDS = 3600 # 1 hour + + +class _SubtitleCache: + def __init__(self): + self._cache = {} + self._lock = threading.Lock() + + def _purge_expired(self): + now = time.monotonic() + expired = [k for k, (_, expiry) in self._cache.items() if now >= expiry] + for k in expired: + del self._cache[k] + + def store(self, subtitle): + """Store a subtitle object and return its cache key (UUID string).""" + key = str(uuid.uuid4()) + expiry = time.monotonic() + _TTL_SECONDS + with self._lock: + self._purge_expired() + self._cache[key] = (subtitle, expiry) + return key + + def get(self, key): + """Return the subtitle object for the given key, or None if not found/expired.""" + with self._lock: + entry = self._cache.get(key) + if entry is None: + return None + subtitle, expiry = entry + if time.monotonic() >= expiry: + del self._cache[key] + return None + return subtitle + + +subtitle_cache = _SubtitleCache() diff --git a/bazarr/subtitles/download.py b/bazarr/subtitles/download.py index 2d44e5ae58..a323240c9e 100644 --- a/bazarr/subtitles/download.py +++ b/bazarr/subtitles/download.py @@ -10,9 +10,8 @@ from subzero.language import Language from subliminal_patch.core import save_subtitles from subliminal_patch.core_persistent import download_best_subtitles -from subliminal_patch.score import ComputeScore -from app.config import settings, get_scores, get_array_from +from app.config import settings, get_array_from from app.database import TableEpisodes, TableMovies, database, select, get_profiles_list from utilities.path_mappings import path_mappings from utilities.helper import get_target_folder, force_unicode @@ -63,6 +62,21 @@ def generate_subtitles(path, languages, audio_language, sceneName, title, media_ saved_any = False if providers: + if job_id: + from app.jobs_queue import jobs_queue as _jq + active_providers = [p for p in providers if p not in pool.discarded_providers] + _provider_count = len(active_providers) + + def _on_provider(provider_name): + try: + idx = active_providers.index(provider_name) + 1 + except ValueError: + idx = 0 + _jq.update_job_progress(job_id=job_id, + progress_message=f"Searching {provider_name} ({idx}/{_provider_count})") + + pool.provider_progress_callback = _on_provider + if forced_minimum_score: min_score = int(forced_minimum_score) + 1 for language in language_set: @@ -79,8 +93,8 @@ def generate_subtitles(path, languages, audio_language, sceneName, title, media_ pool_instance=pool, min_score=int(min_score), hearing_impaired=hi_required, - compute_score=ComputeScore(get_scores()), - use_original_format=original_format in (1, "1", "True", True)) + use_original_format=original_format in (1, "1", "True", True), + use_provider_priority=settings.general.use_provider_priority) except Exception as e: logging.exception(f'BAZARR Error downloading Subtitles for this file {path}: {repr(e)}') return None @@ -121,6 +135,10 @@ def generate_subtitles(path, languages, audio_language, sceneName, title, media_ else: saved_any = True for subtitle in saved_subtitles: + if "hash" in subtitle.matches: + # make matches set cleaner for history purpose when hash matches + subtitle.matches = {match for match in subtitle.matches + if match in ("hash", "hearing_impaired")} processed_subtitle = process_subtitle(subtitle=subtitle, media_type=media_type, audio_language=audio_language, is_upgrade=is_upgrade, is_manual=False, diff --git a/bazarr/subtitles/indexer/movies.py b/bazarr/subtitles/indexer/movies.py index 8e9498653d..cbf1017be6 100644 --- a/bazarr/subtitles/indexer/movies.py +++ b/bazarr/subtitles/indexer/movies.py @@ -277,9 +277,10 @@ def list_missing_subtitles_movies(no=None): event_stream(type='badges') -def movies_full_scan_subtitles(job_id=None, use_cache=None): +def movies_full_scan_subtitles(job_id=None, use_cache=None, wait_for_completion=False): if not job_id: - jobs_queue.add_job_from_function("Indexing all existing movies subtitles", is_progress=True) + jobs_queue.add_job_from_function("Indexing all existing movies subtitles", is_progress=True, + wait_for_completion=wait_for_completion) return if use_cache is None: diff --git a/bazarr/subtitles/indexer/series.py b/bazarr/subtitles/indexer/series.py index 9fe20ba6a2..24ed63f34c 100644 --- a/bazarr/subtitles/indexer/series.py +++ b/bazarr/subtitles/indexer/series.py @@ -282,9 +282,10 @@ def list_missing_subtitles(no=None, epno=None): event_stream(type='badges') -def series_full_scan_subtitles(job_id=None, use_cache=None): +def series_full_scan_subtitles(job_id=None, use_cache=None, wait_for_completion=False): if not job_id: - jobs_queue.add_job_from_function("Indexing all existing episodes subtitles", is_progress=True) + jobs_queue.add_job_from_function("Indexing all existing episodes subtitles", is_progress=True, + wait_for_completion=wait_for_completion) return if use_cache is None: diff --git a/bazarr/subtitles/manual.py b/bazarr/subtitles/manual.py index e943abe1b1..9818291532 100644 --- a/bazarr/subtitles/manual.py +++ b/bazarr/subtitles/manual.py @@ -4,17 +4,15 @@ import os import sys import logging -import pickle -import codecs import subliminal from subzero.language import Language from subliminal_patch.core import save_subtitles from subliminal_patch.core_persistent import list_all_subtitles, download_subtitles -from subliminal_patch.score import ComputeScore +from subliminal_patch.score import compute_score, DEFAULT_SCORES from languages.get_languages import alpha3_from_alpha2 -from app.config import get_scores, settings, get_array_from +from app.config import settings, get_array_from from utilities.helper import get_target_folder, force_unicode from utilities.path_mappings import path_mappings from app.database import (database, get_profiles_list, select, TableEpisodes, TableShows, get_audio_profile_languages, @@ -27,6 +25,7 @@ from subtitles.indexer.movies import store_subtitles_movie from subtitles.processing import ProcessSubtitlesResult +from bazarr.subtitles.cache import subtitle_cache from .pool import update_pools, _get_pool from .utils import get_video, _get_lang_obj, _get_scores, _set_forced_providers from .processing import process_subtitle @@ -44,7 +43,6 @@ def manual_search(path, profile_id, providers, sceneName, title, media_type): also_forced = any([x.forced for x in language_set]) forced_required = all([x.forced for x in language_set]) normal = not also_forced and not forced_required and all([not x.hi for x in language_set]) - compute_score = ComputeScore(get_scores()) _set_forced_providers(pool=pool, also_forced=also_forced, forced_required=forced_required) if providers: @@ -68,6 +66,7 @@ def manual_search(path, profile_id, providers, sceneName, title, media_type): subtitles_list = [] minimum_score = settings.general.minimum_score minimum_score_movie = settings.general.minimum_score_movie + score_handler = DEFAULT_SCORES['episode'] if media_type == "series" else DEFAULT_SCORES['movie'] for s in subtitles[video]: if not normal and s.language not in language_set: @@ -75,7 +74,9 @@ def manual_search(path, profile_id, providers, sceneName, title, media_type): continue try: - matches = s.get_matches(video) + matches = s.matches if hasattr(s, 'matches') and isinstance(s.matches, set) and len(s.matches) \ + else s.get_matches(video) + matches = {match for match in matches if match in score_handler.keys()} # cleanup unwanted criterion except AttributeError: continue @@ -100,8 +101,10 @@ def manual_search(path, profile_id, providers, sceneName, title, media_type): if 'hash' not in matches: not_matched = scores - matches + not_matched = {match for match in not_matched if match in score_handler.keys()} s.score = score_without_hash else: + matches = s.matches = {match for match in matches if match in ("hash", "hearing_impaired")} s.score = score not_matched = set() @@ -128,7 +131,7 @@ def manual_search(path, profile_id, providers, sceneName, title, media_type): language=str(s.language.basename), hearing_impaired=str(s.hearing_impaired), provider=s.provider_name, - subtitle=codecs.encode(pickle.dumps(s.make_picklable()), "base64").decode(), + subtitle=subtitle_cache.store(s), url=s.page_link, original_format=s.use_original_format, matches=list(matches), @@ -156,7 +159,10 @@ def manual_download_subtitle(path, audio_language, hi, forced, subtitle, provide else: os.environ["SZ_KEEP_ENCODING"] = "True" - subtitle = pickle.loads(codecs.decode(subtitle.encode(), "base64")) + subtitle = subtitle_cache.get(subtitle) + if subtitle is None: + logging.error("BAZARR Subtitle not found in cache (expired or invalid ID)") + return 'Subtitle not found in cache. Please search again.' if hi == 'True': subtitle.language.hi = True else: diff --git a/bazarr/subtitles/mass_download/movies.py b/bazarr/subtitles/mass_download/movies.py index 5040022f88..cb367b005f 100644 --- a/bazarr/subtitles/mass_download/movies.py +++ b/bazarr/subtitles/mass_download/movies.py @@ -84,6 +84,7 @@ def movies_download_subtitles(no, job_id=None, job_sub_function=False): providers_list = get_providers() + downloaded_count = 0 if providers_list: for language in ast.literal_eval(movie.missing_subtitles): if language is not None: @@ -107,16 +108,21 @@ def movies_download_subtitles(no, job_id=None, job_sub_function=False): store_subtitles_movie(movie.path, moviePath) history_log_movie(1, no, result) send_notifications_movie(no, result.message) + downloaded_count += 1 + outcome_msg = (f"{downloaded_count} subtitle(s) downloaded" + if downloaded_count else "No subtitles found") else: logging.info("BAZARR All providers are throttled") + outcome_msg = "All providers throttled" - jobs_queue.update_job_progress(job_id=job_id, progress_value="max") + jobs_queue.update_job_progress(job_id=job_id, progress_value="max", + progress_message=outcome_msg) jobs_queue.update_job_name(job_id=job_id, new_job_name=f"Downloaded missing subtitles for {movie.title} ({movie.year})") def movie_download_specific_subtitles(radarr_id, language, hi, forced, job_id=None): if not job_id: - return jobs_queue.add_job_from_function("Searching subtitles", progress_max=1, is_progress=False) + return jobs_queue.add_job_from_function("Searching subtitles", is_progress=True) movieInfo = database.execute( select( @@ -147,6 +153,7 @@ def movie_download_specific_subtitles(radarr_id, language, hi, forced, job_id=No language_str = language jobs_queue.update_job_name(job_id=job_id, new_job_name=f"Searching {language_str.upper()} for {title}") + jobs_queue.update_job_progress(job_id=job_id, progress_message="Preparing search...") audio_language_list = get_audio_profile_languages(movieInfo.audio_language) if len(audio_language_list) > 0: @@ -165,7 +172,11 @@ def movie_download_specific_subtitles(radarr_id, language, hi, forced, job_id=No history_log_movie(1, radarr_id, result) send_notifications_movie(radarr_id, result.message) store_subtitles_movie(result.path, moviePath) + jobs_queue.update_job_progress(job_id=job_id, progress_value='max', + progress_message="Subtitle downloaded") else: + jobs_queue.update_job_progress(job_id=job_id, progress_value='max', + progress_message="No subtitles found") event_stream(type='movie', payload=radarr_id) return '', 204 except OSError: diff --git a/bazarr/subtitles/mass_download/series.py b/bazarr/subtitles/mass_download/series.py index 8e6679f9c1..892035810f 100644 --- a/bazarr/subtitles/mass_download/series.py +++ b/bazarr/subtitles/mass_download/series.py @@ -50,6 +50,7 @@ def series_download_subtitles(no, job_id=None, job_sub_function=False): .join(TableShows) .where(reduce(operator.and_, conditions))) \ .all() + throttled = False if not episodes_details: logging.debug(f"BAZARR no episode for that sonarrSeriesId have been found in database or they have all been " f"ignored because of monitored status, series type or series tags: {no}") @@ -70,8 +71,12 @@ def series_download_subtitles(no, job_id=None, job_sub_function=False): else: jobs_queue.update_job_progress(job_id=job_id, progress_value=count_episodes_details) logging.info("BAZARR All providers are throttled") + throttled = True break + outcome_msg = ("All providers throttled" if throttled + else "Search completed") + jobs_queue.update_job_progress(job_id=job_id, progress_message=outcome_msg) jobs_queue.update_job_name(job_id=job_id, new_job_name=f"Downloaded missing subtitles for {series_row.title}") @@ -126,6 +131,7 @@ def episode_download_subtitles(no, job_id=None, job_sub_function=False, provider if not providers_list: providers_list = get_providers() + downloaded_count = 0 if providers_list: audio_language_list = get_audio_profile_languages(episode.audio_language) if len(audio_language_list) > 0: @@ -162,17 +168,22 @@ def episode_download_subtitles(no, job_id=None, job_sub_function=False, provider store_subtitles(episode.path, path_mappings.path_replace(episode.path)) history_log(1, episode.sonarrSeriesId, episode.sonarrEpisodeId, result) send_notifications(episode.sonarrSeriesId, episode.sonarrEpisodeId, result.message) + downloaded_count += 1 + outcome_msg = (f"{downloaded_count} subtitle(s) downloaded" + if downloaded_count else "No subtitles found") else: logging.info("BAZARR All providers are throttled") + outcome_msg = "All providers throttled" if not job_sub_function and job_id: - jobs_queue.update_job_progress(job_id=job_id, progress_value='max') + jobs_queue.update_job_progress(job_id=job_id, progress_value='max', + progress_message=outcome_msg) jobs_queue.update_job_name(job_id=job_id, new_job_name=f"Downloaded missing subtitles for {episode.title}") def episode_download_specific_subtitles(sonarr_series_id, sonarr_episode_id, language, hi, forced, job_id=None): if not job_id: - return jobs_queue.add_job_from_function("Searching subtitles", progress_max=1, is_progress=False) + return jobs_queue.add_job_from_function("Searching subtitles", is_progress=True) episodeInfo = database.execute( select(TableEpisodes.path, @@ -210,6 +221,7 @@ def episode_download_specific_subtitles(sonarr_series_id, sonarr_episode_id, lan jobs_queue.update_job_name(job_id=job_id, new_job_name=f"Searching {language_str.upper()} for {episode_long_title}") + jobs_queue.update_job_progress(job_id=job_id, progress_message="Preparing search...") audio_language_list = get_audio_profile_languages(episodeInfo.audio_language) if len(audio_language_list) > 0: @@ -228,7 +240,11 @@ def episode_download_specific_subtitles(sonarr_series_id, sonarr_episode_id, lan history_log(1, sonarr_series_id, sonarr_episode_id, result) send_notifications(sonarr_series_id, sonarr_episode_id, result.message) store_subtitles(result.path, episodePath) + jobs_queue.update_job_progress(job_id=job_id, progress_value='max', + progress_message="Subtitle downloaded") else: + jobs_queue.update_job_progress(job_id=job_id, progress_value='max', + progress_message="No subtitles found") event_stream(type='episode', payload=sonarr_episode_id) return '', 204 except OSError: diff --git a/bazarr/subtitles/mass_operations.py b/bazarr/subtitles/mass_operations.py new file mode 100644 index 0000000000..d5dda58d85 --- /dev/null +++ b/bazarr/subtitles/mass_operations.py @@ -0,0 +1,517 @@ +# coding=utf-8 + +import ast +import logging +import os + +from app.config import settings +from app.database import TableEpisodes, TableMovies, TableHistory, TableHistoryMovie, database, select +from app.jobs_queue import jobs_queue +from subtitles.sync import sync_subtitles +from subtitles.tools.mods import subtitles_apply_mods +from subtitles.indexer.series import series_scan_subtitles +from subtitles.indexer.movies import movies_scan_subtitles +from subtitles.mass_download.series import series_download_subtitles +from subtitles.mass_download.movies import movies_download_subtitles +from subtitles.upgrade import upgrade_episodes_subtitles, upgrade_movies_subtitles +from utilities.path_mappings import path_mappings +from utilities.video_analyzer import languages_from_colon_seperated_string +from sqlalchemy import or_ + +logger = logging.getLogger(__name__) + +VALID_ACTIONS = { + 'sync', 'translate', 'OCR_fixes', 'common', 'remove_HI', + 'remove_tags', 'fix_uppercase', 'reverse_rtl', 'emoji', + 'scan-disk', 'search-missing', 'upgrade', +} + +MEDIA_ACTIONS = {'scan-disk', 'search-missing', 'upgrade'} + +MOD_ACTIONS = {'OCR_fixes', 'common', 'remove_HI', 'remove_tags', 'fix_uppercase', 'reverse_rtl', 'emoji'} + + +def _parse_subtitles_column(subtitles_raw): + """Parse the subtitles TEXT column into a list of (lang_string, path) tuples.""" + if not subtitles_raw: + return [] + try: + parsed = ast.literal_eval(subtitles_raw) + return [(entry[0], entry[1]) for entry in parsed if len(entry) >= 2 and entry[1]] + except (ValueError, SyntaxError): + return [] + + +def _get_synced_episode_paths(): + """Get set of subtitle paths that have been synced (action=5) from episode history.""" + results = database.execute( + select(TableHistory.subtitles_path) + .where(TableHistory.action == 5) + ).all() + return {r.subtitles_path for r in results if r.subtitles_path} + + +def _get_synced_movie_paths(): + """Get set of subtitle paths that have been synced (action=5) from movie history.""" + results = database.execute( + select(TableHistoryMovie.subtitles_path) + .where(TableHistoryMovie.action == 5) + ).all() + return {r.subtitles_path for r in results if r.subtitles_path} + + +def _collect_subtitle_items(items, action, options): + """Collect subtitle items from the database for processing. + + Args: + items: List of dicts with 'type' and IDs, or None to collect entire library. + action: The action to perform (sync, translate, mod, etc.). + options: Dict with force_resync, max_offset_seconds, gss, no_fix_framerate. + + Returns: + Tuple of (items_list, skipped_count). + """ + options = options or {} + force_resync = options.get('force_resync', False) + max_offset = str(options.get('max_offset_seconds', settings.subsync.max_offset_seconds)) + gss = options.get('gss', settings.subsync.gss) + no_fix_framerate = options.get('no_fix_framerate', settings.subsync.no_fix_framerate) + + # Parse item types + series_ids = [] + episode_ids = [] + movie_ids = [] + + if items is None: + # Entire library mode + pass + else: + for item in items: + item_type = item.get('type') + if item_type == 'series': + sid = item.get('sonarrSeriesId') + if sid is not None: + series_ids.append(sid) + elif item_type == 'episode': + eid = item.get('sonarrEpisodeId') + if eid is not None: + episode_ids.append(eid) + elif item_type == 'movie': + rid = item.get('radarrId') + if rid is not None: + movie_ids.append(rid) + + all_items = [] + total_skipped = 0 + target_lang = options.get('to_lang') if action == 'translate' else None + source_lang = options.get('from_lang') if action == 'translate' else None + + # Collect episode subtitles + should_collect_episodes = (items is None and settings.general.use_sonarr) or series_ids or episode_ids + if should_collect_episodes: + ep_items, ep_skipped = _collect_episodes( + series_ids=series_ids or None, + episode_ids=episode_ids or None, + action=action, + force_resync=force_resync, + max_offset=max_offset, + gss=gss, + no_fix_framerate=no_fix_framerate, + target_lang=target_lang, + source_lang=source_lang, + ) + all_items.extend(ep_items) + total_skipped += ep_skipped + + # Collect movie subtitles + should_collect_movies = (items is None and settings.general.use_radarr) or movie_ids + if should_collect_movies: + mov_items, mov_skipped = _collect_movies( + movie_ids=movie_ids or None, + action=action, + force_resync=force_resync, + max_offset=max_offset, + gss=gss, + no_fix_framerate=no_fix_framerate, + target_lang=target_lang, + source_lang=source_lang, + ) + all_items.extend(mov_items) + total_skipped += mov_skipped + + return all_items, total_skipped + + +def _collect_episodes(series_ids=None, episode_ids=None, action='sync', + force_resync=False, max_offset='60', gss=True, no_fix_framerate=True, + target_lang=None, source_lang=None): + """Collect episode subtitles from the database.""" + query = select( + TableEpisodes.sonarrEpisodeId, + TableEpisodes.sonarrSeriesId, + TableEpisodes.path, + TableEpisodes.subtitles, + ) + + filters = [] + if episode_ids: + filters.append(TableEpisodes.sonarrEpisodeId.in_(episode_ids)) + if series_ids: + filters.append(TableEpisodes.sonarrSeriesId.in_(series_ids)) + if filters: + query = query.where(or_(*filters)) + + episodes = database.execute(query).all() + + synced_paths = set() + if action == 'sync' and not force_resync: + synced_paths = _get_synced_episode_paths() + + items = [] + skipped = 0 + + for ep in episodes: + subtitles = _parse_subtitles_column(ep.subtitles) + video_path = path_mappings.path_replace(ep.path) + + # For translate: check if target language already exists + if action == 'translate' and target_lang: + existing_langs = {lang_str.split(':')[0] for lang_str, _ in subtitles} + if target_lang in existing_langs: + skipped += 1 + continue + + for lang_string, sub_path in subtitles: + lang_info = languages_from_colon_seperated_string(lang_string) + + # Forced subs can't be synced or translated, but mods are fine + if lang_info['forced'] and action in ('sync', 'translate'): + skipped += 1 + continue + + # For translate: only queue subtitles matching the requested source language + sub_lang = lang_string.split(':')[0] + if action == 'translate' and source_lang and sub_lang != source_lang: + skipped += 1 + continue + + mapped_sub_path = path_mappings.path_replace(sub_path) + if not os.path.isfile(mapped_sub_path): + skipped += 1 + continue + + if action == 'sync' and not force_resync: + reversed_path = path_mappings.path_replace_reverse(mapped_sub_path) + if reversed_path in synced_paths: + skipped += 1 + continue + + items.append({ + 'video_path': video_path, + 'srt_path': mapped_sub_path, + 'srt_lang': sub_lang, + 'forced': lang_info['forced'], + 'hi': lang_info['hi'], + 'sonarr_series_id': ep.sonarrSeriesId, + 'sonarr_episode_id': ep.sonarrEpisodeId, + 'radarr_id': None, + 'max_offset_seconds': max_offset, + 'no_fix_framerate': no_fix_framerate, + 'gss': gss, + }) + + return items, skipped + + +def _collect_movies(movie_ids=None, action='sync', force_resync=False, + max_offset='60', gss=True, no_fix_framerate=True, + target_lang=None, source_lang=None): + """Collect movie subtitles from the database.""" + query = select( + TableMovies.radarrId, + TableMovies.path, + TableMovies.subtitles, + ) + + if movie_ids: + query = query.where(TableMovies.radarrId.in_(movie_ids)) + + movies = database.execute(query).all() + + synced_paths = set() + if action == 'sync' and not force_resync: + synced_paths = _get_synced_movie_paths() + + items = [] + skipped = 0 + + for movie in movies: + subtitles = _parse_subtitles_column(movie.subtitles) + video_path = path_mappings.path_replace_movie(movie.path) + + # For translate: check if target language already exists + if action == 'translate' and target_lang: + existing_langs = {lang_str.split(':')[0] for lang_str, _ in subtitles} + if target_lang in existing_langs: + skipped += 1 + continue + + for lang_string, sub_path in subtitles: + lang_info = languages_from_colon_seperated_string(lang_string) + + # Forced subs can't be synced or translated, but mods are fine + if lang_info['forced'] and action in ('sync', 'translate'): + skipped += 1 + continue + + # For translate: only queue subtitles matching the requested source language + sub_lang = lang_string.split(':')[0] + if action == 'translate' and source_lang and sub_lang != source_lang: + skipped += 1 + continue + + mapped_sub_path = path_mappings.path_replace_movie(sub_path) + if not os.path.isfile(mapped_sub_path): + skipped += 1 + continue + + if action == 'sync' and not force_resync: + reversed_path = path_mappings.path_replace_reverse_movie(mapped_sub_path) + if reversed_path in synced_paths: + skipped += 1 + continue + + items.append({ + 'video_path': video_path, + 'srt_path': mapped_sub_path, + 'srt_lang': sub_lang, + 'forced': lang_info['forced'], + 'hi': lang_info['hi'], + 'sonarr_series_id': None, + 'sonarr_episode_id': None, + 'radarr_id': movie.radarrId, + 'max_offset_seconds': max_offset, + 'no_fix_framerate': no_fix_framerate, + 'gss': gss, + }) + + return items, skipped + + +def _process_subtitle_item(item, action, options, job_id): + """Process a single subtitle item based on the action. + + Returns True on success, False on failure. + """ + if action == 'sync': + return sync_subtitles( + video_path=item['video_path'], + srt_path=item['srt_path'], + srt_lang=item['srt_lang'], + forced=item['forced'], + hi=item['hi'], + percent_score=0, + sonarr_series_id=item['sonarr_series_id'], + sonarr_episode_id=item['sonarr_episode_id'], + radarr_id=item['radarr_id'], + max_offset_seconds=item['max_offset_seconds'], + no_fix_framerate=item['no_fix_framerate'], + gss=item['gss'], + force_sync=True, + job_id=job_id, + ) + elif action == 'translate': + from subtitles.tools.translate.main import translate_subtitles_file + media_type = 'series' if item['sonarr_series_id'] else 'movies' + # Don't pass the batch job_id to translate. translate_subtitles_file + # has its own job/progress lifecycle that would hijack the batch job. + # Calling without job_id makes it queue as its own separate job. + translate_subtitles_file( + video_path=item['video_path'], + source_srt_file=item['srt_path'], + from_lang=options.get('from_lang', item['srt_lang']), + to_lang=options.get('to_lang', 'en'), + forced=item['forced'], + hi=item['hi'], + media_type=media_type, + sonarr_series_id=item['sonarr_series_id'], + sonarr_episode_id=item['sonarr_episode_id'], + radarr_id=item['radarr_id'], + ) + return True + elif action in MOD_ACTIONS: + subtitles_apply_mods( + item['srt_lang'], + item['srt_path'], + [action], + item['video_path'], + ) + return True + return False + + +def _process_media_action(items, action, job_id): + """Handle scan-disk, search-missing, and upgrade actions for series/movies. + + Args: + items: List of dicts with 'type' and IDs. + action: 'scan-disk', 'search-missing', or 'upgrade'. + job_id: Job ID for progress tracking. + + Returns: + Dict with queued, skipped, errors. + """ + queued = 0 + skipped = 0 + errors = [] + + if action == 'upgrade': + sonarr_series_ids = [i.get('sonarrSeriesId') for i in items + if i.get('type') in ('series', 'episode') and i.get('sonarrSeriesId')] + radarr_ids = [i.get('radarrId') for i in items + if i.get('type') == 'movie' and i.get('radarrId')] + try: + if sonarr_series_ids: + upgrade_episodes_subtitles(job_id=job_id, sonarr_series_ids=sonarr_series_ids) + if radarr_ids: + upgrade_movies_subtitles(job_id=job_id, radarr_ids=radarr_ids) + queued = len(sonarr_series_ids) + len(radarr_ids) + except Exception as e: + logger.error(f'Error during upgrade: {e}') + errors.append(str(e)) + return {'queued': queued, 'skipped': 0, 'errors': errors} + + jobs_queue.update_job_progress(job_id=job_id, progress_max=len(items)) + + for i, item in enumerate(items, start=1): + item_type = item.get('type') + jobs_queue.update_job_progress( + job_id=job_id, + progress_value=i, + progress_message=f"Processing {item_type} ({i}/{len(items)})" + ) + + try: + if action == 'scan-disk': + if item_type in ('series', 'episode'): + series_id = item.get('sonarrSeriesId') + if not series_id: + skipped += 1 + continue + series_scan_subtitles(series_id) + elif item_type == 'movie': + radarr_id = item.get('radarrId') + if not radarr_id: + skipped += 1 + continue + movies_scan_subtitles(radarr_id) + else: + skipped += 1 + continue + elif action == 'search-missing': + if item_type in ('series', 'episode'): + series_id = item.get('sonarrSeriesId') + if not series_id: + skipped += 1 + continue + series_download_subtitles(series_id) + elif item_type == 'movie': + radarr_id = item.get('radarrId') + if not radarr_id: + skipped += 1 + continue + movies_download_subtitles(radarr_id) + else: + skipped += 1 + continue + queued += 1 + except Exception as e: + logger.error(f'Error processing {action} for {item}: {e}') + errors.append(str(e)) + + return {'queued': queued, 'skipped': skipped, 'errors': errors} + + +def mass_batch_operation(items=None, action='sync', options=None, job_id=None): + """Main entry point for all batch operations on subtitles. + + Handles sync, translate, subtitle mods, scan-disk, and search-missing + in a unified interface. Runs as a single job with progress tracking, + processing items sequentially. + + Args: + items: List of dicts with 'type' and IDs. If None, processes entire library. + action: One of VALID_ACTIONS. + options: Dict with action-specific options (force_resync, from_lang, to_lang, etc.). + job_id: Job ID for scheduled task tracking. + + Returns: + Dict with queued, skipped, errors. Or None if scheduling a job. + """ + if action not in VALID_ACTIONS: + return {'queued': 0, 'skipped': 0, 'errors': [f'Invalid action: {action}']} + + options = options or {} + + # When called without a job_id (e.g. from the scheduler), create one so that + # downstream functions like sync_subtitles run inline instead of re-queuing + # themselves as individual jobs. + if not job_id: + jobs_queue.add_job_from_function( + f"Mass {action.replace('_', ' ').replace('-', ' ').title()} " + f"({'Library' if items is None else f'{len(items)} items'})", + is_progress=True, + ) + return + + # Media actions (scan-disk, search-missing) work on media items directly + if action in MEDIA_ACTIONS: + if not items: + return {'queued': 0, 'skipped': 0, 'errors': []} + return _process_media_action(items, action, job_id) + + # Subtitle actions: collect subtitle files, then process them + if items is not None and len(items) == 0: + jobs_queue.update_job_progress(job_id=job_id, progress_max=0) + return {'queued': 0, 'skipped': 0, 'errors': []} + + all_items, total_skipped = _collect_subtitle_items(items, action, options) + + # Process items sequentially within this single job + total_count = len(all_items) + jobs_queue.update_job_progress(job_id=job_id, progress_max=total_count) + + if total_count == 0: + jobs_queue.update_job_progress(job_id=job_id, progress_value='max') + + processed = 0 + failed = 0 + all_errors = [] + + for i, item in enumerate(all_items, start=1): + jobs_queue.update_job_progress( + job_id=job_id, + progress_value=i, + progress_message=f"{action}: {os.path.basename(item['srt_path'])} ({i}/{total_count})" + ) + + try: + result = _process_subtitle_item(item, action, options, job_id) + if result: + processed += 1 + else: + failed += 1 + except Exception as e: + logger.error(f'Error during {action} on {item["srt_path"]}: {e}') + all_errors.append(str(e)) + failed += 1 + + jobs_queue.update_job_name( + job_id=job_id, + new_job_name=f"Mass {action} complete: {processed} done, {total_skipped} skipped" + ) + logger.info( + f'BAZARR mass {action} complete: {processed} processed, {failed} failed, ' + f'{total_skipped} skipped, {len(all_errors)} errors' + ) + return {'queued': processed, 'skipped': total_skipped + failed, 'errors': all_errors} diff --git a/bazarr/subtitles/sync.py b/bazarr/subtitles/sync.py index ca098d6dbf..ef14039430 100644 --- a/bazarr/subtitles/sync.py +++ b/bazarr/subtitles/sync.py @@ -23,7 +23,8 @@ def sync_subtitles(video_path, gss=settings.subsync.gss, no_fix_framerate=settings.subsync.no_fix_framerate, reference=None, - force_sync=False): + force_sync=False, + callback=None): if not settings.subsync.use_subsync and not force_sync: logging.debug('BAZARR automatic syncing is disabled in settings. Skipping sync routine.') return False @@ -66,6 +67,8 @@ def sync_subtitles(video_path, } try: subsync.sync(**sync_kwargs) + if callback: + callback() except Exception: logging.exception(f'BAZARR an unhandled exception occurs during the synchronization process for this ' f'subtitle file: {srt_path}') diff --git a/bazarr/subtitles/tools/mods.py b/bazarr/subtitles/tools/mods.py index 8d0536d0fb..302b816a18 100644 --- a/bazarr/subtitles/tools/mods.py +++ b/bazarr/subtitles/tools/mods.py @@ -1,6 +1,7 @@ # coding=utf-8 import os +import sys import logging from subliminal_patch.subtitle import Subtitle @@ -8,11 +9,93 @@ from subzero.language import Language from app.config import settings +from app.jobs_queue import jobs_queue from languages.custom_lang import CustomLanguage from languages.get_languages import alpha3_from_alpha2 from subtitles.indexer.utils import get_external_subtitles_path +MOD_LABELS = { + 'remove_HI': 'Remove HI Tags', + 'remove_tags': 'Remove Style Tags', + 'OCR_fixes': 'OCR Fixes', + 'common': 'Common Fixes', + 'fix_uppercase': 'Fix Uppercase', + 'reverse_rtl': 'Reverse RTL', + 'add_color': 'Add Color', + 'change_frame_rate': 'Change Frame Rate', + 'adjust_time': 'Adjust Times', + 'emoji': 'Remove Emoji', +} + + +def apply_subtitle_mods(language, subtitle_path, mods, video_path, + media_type=None, media_id=None, job_id=None): + """Job-aware wrapper for subtitles_apply_mods. + + When called without a job_id, queues the work as a backend job and returns + immediately. When called with a job_id (by the job queue consumer), does the + actual work and handles post-processing (store_subtitles, event_stream, chmod). + """ + if not job_id: + # No local variables can be assigned before add_job_from_function because + # it introspects the frame and re-passes all locals as kwargs on re-invocation. + jobs_queue.add_job_from_function( + (lambda m, p: f'{MOD_LABELS.get(m, m)}: {os.path.basename(p)}')( + mods[0] if mods else 'mods', subtitle_path), + is_progress=False, + ) + return + + mod_label = MOD_LABELS.get(mods[0], mods[0]) if mods else 'Apply Mods' + filename = os.path.basename(subtitle_path) + + try: + subtitles_apply_mods(language=language, subtitle_path=subtitle_path, + mods=mods, video_path=video_path) + except Exception: + jobs_queue.update_job_name( + job_id=job_id, + new_job_name=f'Failed {mod_label}: {filename}', + ) + raise + + # apply chmod if required + chmod = int(settings.general.chmod, 8) if not sys.platform.startswith( + 'win') and settings.general.chmod_enabled else None + if chmod and os.path.exists(subtitle_path): + os.chmod(subtitle_path, chmod) + + # re-index subtitles so Bazarr's DB picks up the changes + from subtitles.indexer.series import store_subtitles + from subtitles.indexer.movies import store_subtitles_movie + from app.event_handler import event_stream + from utilities.path_mappings import path_mappings + + if media_type == 'episode': + store_subtitles(path_mappings.path_replace_reverse(video_path), video_path) + elif media_type == 'movie': + store_subtitles_movie(path_mappings.path_replace_reverse_movie(video_path), video_path) + + if media_id and media_type: + if media_type == 'episode': + from app.database import TableEpisodes, database, select + metadata = database.execute( + select(TableEpisodes.sonarrSeriesId) + .where(TableEpisodes.sonarrEpisodeId == media_id) + ).first() + if metadata: + event_stream(type='series', payload=metadata.sonarrSeriesId) + event_stream(type='episode', payload=media_id) + else: + event_stream(type='movie', payload=media_id) + + jobs_queue.update_job_name( + job_id=job_id, + new_job_name=f'{mod_label}: {filename}', + ) + + def subtitles_apply_mods(language, subtitle_path, mods, video_path): language = alpha3_from_alpha2(language) custom = CustomLanguage.from_value(language, "alpha3") diff --git a/bazarr/subtitles/tools/score.py b/bazarr/subtitles/tools/score.py deleted file mode 100644 index 11771354ef..0000000000 --- a/bazarr/subtitles/tools/score.py +++ /dev/null @@ -1,140 +0,0 @@ -# coding=utf-8 - -from __future__ import annotations - -import logging - -from app.config import get_settings - -logger = logging.getLogger(__name__) - - -class Score: - media = None - defaults = {} - - def __init__(self, load_profiles=False, **kwargs): - self.data = self.defaults.copy() - self.data.update(**kwargs) - self.data["hash"] = self._hash_score() - self._profiles = [] - self._profiles_loaded = False - - if load_profiles: - self.load_profiles() - - def check_custom_profiles(self, subtitle, matches): - if not self._profiles_loaded: - self.load_profiles() - - for profile in self._profiles: - if profile.check(subtitle): - matches.add(profile.name) - - def load_profiles(self): - self._profiles = [] - - if self._profiles: - logger.debug("Loaded profiles: %s", self._profiles) - else: - logger.debug("No score profiles found") - self._profiles = [] - - self._profiles_loaded = True - - def reset(self): - self.data.update(self.defaults) - - def update(self, **kwargs): - self.data.update(kwargs) - - @classmethod - def from_config(cls, **kwargs): - return cls(True, **kwargs) - - def get_scores(self, min_percent, special=None): - return ( - self.max_score * (special or min_percent) / 100, - self.max_score, - set(list(self.scores.keys())), - ) - - @property - def custom_profile_scores(self): - return {item.name: item.score for item in self._profiles} - - @property - def scores(self): - return {**self.custom_profile_scores, **self.data} - - @property - def max_score(self): - return ( - self.data["hash"] - + self.data["hearing_impaired"] - + sum(item.score for item in self._profiles if item.score) - ) - - def _hash_score(self): - return sum( - val - for key, val in self.data.items() - if key not in ("hash", "hearing_impaired") - ) - - def __str__(self): - return f"<{self.media} Score class>" - - -class SeriesScore(Score): - media = "series" - defaults = { - "hash": 359, - "series": 180, - "year": 90, - "season": 30, - "episode": 30, - "release_group": 15, - "source": 7, - "audio_codec": 3, - "resolution": 2, - "video_codec": 2, - "hearing_impaired": 1, - "streaming_service": 0, - "edition": 0, - } - - @classmethod - def from_config(cls, **kwargs): - return cls(True, **kwargs["series_scores"]) - - def update(self, **kwargs): - self.data.update(kwargs["series_scores"]) - - -class MovieScore(Score): - media = "movies" - defaults = { - "hash": 119, - "title": 60, - "year": 30, - "release_group": 15, - "source": 7, - "audio_codec": 3, - "resolution": 2, - "video_codec": 2, - "hearing_impaired": 1, - "streaming_service": 0, - "edition": 0, - } - - @classmethod - def from_config(cls, **kwargs): - return cls(True, **kwargs["movie_scores"]) - - def update(self, **kwargs): - self.data.update(kwargs["movie_scores"]) - - -series_score = SeriesScore.from_config(**get_settings()) -movie_score = MovieScore.from_config(**get_settings()) diff --git a/bazarr/subtitles/tools/translate/batch.py b/bazarr/subtitles/tools/translate/batch.py index 7290de716e..b80feaa818 100644 --- a/bazarr/subtitles/tools/translate/batch.py +++ b/bazarr/subtitles/tools/translate/batch.py @@ -4,9 +4,14 @@ import os import ast import re -from app.database import TableEpisodes, TableMovies, database, select +import subprocess +from app.database import TableEpisodes, TableMovies, TableShows, database, select +from app.jobs_queue import jobs_queue from app.event_handler import event_stream from utilities.path_mappings import path_mappings +from utilities.binaries import get_binary +from utilities.video_analyzer import parse_video_metadata, _handle_alpha3 +from languages.get_languages import alpha3_from_alpha2 from subtitles.indexer.series import store_subtitles from subtitles.indexer.movies import store_subtitles_movie from subtitles.tools.translate.main import translate_subtitles_file @@ -24,7 +29,8 @@ def process_episode_translation(item, source_language, target_language, forced, # Get episode info from database episode = database.execute( - select(TableEpisodes.path, TableEpisodes.subtitles, TableEpisodes.sonarrSeriesId) + select(TableEpisodes.path, TableEpisodes.subtitles, TableEpisodes.sonarrSeriesId, + TableEpisodes.title, TableEpisodes.season, TableEpisodes.episode) .where(TableEpisodes.sonarrEpisodeId == sonarr_episode_id) ).first() @@ -32,6 +38,20 @@ def process_episode_translation(item, source_language, target_language, forced, logger.error(f'Episode {sonarr_episode_id} not found') return False + # Get series title + show = database.execute( + select(TableShows.title).where(TableShows.sonarrSeriesId == sonarr_series_id) + ).first() + show_title = show.title if show else 'Unknown' + + # Update job name with actual episode info + if job_id: + ep_label = f'{show_title} S{episode.season:02d}E{episode.episode:02d}' + jobs_queue.update_job_name( + job_id=job_id, + new_job_name=f'Translating {ep_label} ({source_language.upper()} โ†’ {target_language.upper()})' + ) + video_path = path_mappings.path_replace(episode.path) # Find source subtitle @@ -52,7 +72,7 @@ def process_episode_translation(item, source_language, target_language, forced, # Queue translation try: - result = translate_subtitles_file( + translate_subtitles_file( video_path=video_path, source_srt_file=source_subtitle_path, from_lang=source_language, @@ -65,14 +85,12 @@ def process_episode_translation(item, source_language, target_language, forced, radarr_id=None, job_id=job_id ) - if result is not False: - # Re-index subtitles so Bazarr's DB knows about the new translated file - store_subtitles(path_mappings.path_replace_reverse(video_path), video_path) - # Notify frontend to refresh - event_stream(type='series', payload=sonarr_series_id) - event_stream(type='episode', payload=sonarr_episode_id) - return True - return False + # Re-index subtitles so Bazarr's DB knows about the new translated file + store_subtitles(path_mappings.path_replace_reverse(video_path), video_path) + # Notify frontend to refresh series and episode views + event_stream(type='series', payload=sonarr_series_id) + event_stream(type='episode', payload=sonarr_episode_id) + return True except Exception as e: logger.error(f'Translation failed for episode {sonarr_episode_id}: {e}') return False @@ -87,7 +105,7 @@ def process_movie_translation(item, source_language, target_language, forced, hi # Get movie info from database movie = database.execute( - select(TableMovies.path, TableMovies.subtitles) + select(TableMovies.path, TableMovies.subtitles, TableMovies.title) .where(TableMovies.radarrId == radarr_id) ).first() @@ -95,6 +113,13 @@ def process_movie_translation(item, source_language, target_language, forced, hi logger.error(f'Movie {radarr_id} not found') return False + # Update job name with actual movie title + if job_id: + jobs_queue.update_job_name( + job_id=job_id, + new_job_name=f'Translating {movie.title} ({source_language.upper()} โ†’ {target_language.upper()})' + ) + video_path = path_mappings.path_replace_movie(movie.path) # Find source subtitle @@ -115,7 +140,7 @@ def process_movie_translation(item, source_language, target_language, forced, hi # Queue translation try: - result = translate_subtitles_file( + translate_subtitles_file( video_path=video_path, source_srt_file=source_subtitle_path, from_lang=source_language, @@ -128,13 +153,11 @@ def process_movie_translation(item, source_language, target_language, forced, hi radarr_id=radarr_id, job_id=job_id ) - if result is not False: - # Re-index subtitles so Bazarr's DB knows about the new translated file - store_subtitles_movie(path_mappings.path_replace_reverse_movie(video_path), video_path) - # Notify frontend to refresh - event_stream(type='movie', payload=radarr_id) - return True - return False + # Re-index subtitles so Bazarr's DB knows about the new translated file + store_subtitles_movie(path_mappings.path_replace_reverse_movie(video_path), video_path) + # Notify frontend to refresh movie view + event_stream(type='movie', payload=radarr_id) + return True except Exception as e: logger.error(f'Translation failed for movie {radarr_id}: {e}') return False @@ -215,21 +238,173 @@ def sort_key(sub): if resolved_path: return resolved_path, sub['code2'] - # Third pass: Scan filesystem fallback + # Third pass: Extract embedded subtitles from video container + if subtitles and isinstance(subtitles, list): + embedded_subs = [] + for sub in subtitles: + if isinstance(sub, (list, tuple)) and len(sub) >= 2: + lang_parts = sub[0].split(':') + sub_code = lang_parts[0] + sub_path = sub[1] + if sub_path is None: # Embedded subtitle (no file on disk) + embedded_subs.append({ + 'code2': sub_code, + 'hi': len(lang_parts) > 1 and lang_parts[1].lower() == 'hi', + 'forced': len(lang_parts) > 1 and lang_parts[1].lower() == 'forced', + }) + + if embedded_subs: + # Prefer exact language match, then fall back to any (preferring English) + candidates = [s for s in embedded_subs if s['code2'] == language_code] + if not candidates: + common_languages = ['en', 'eng'] + candidates = sorted(embedded_subs, + key=lambda x: (x['forced'], x['hi'], x['code2'] not in common_languages)) + + for sub in candidates: + extracted_path = extract_embedded_subtitle(video_path, sub['code2'], media_type) + if extracted_path: + return extracted_path, sub['code2'] + + # Fourth pass: Scan filesystem fallback filesystem_subs = scan_filesystem_for_subtitles(video_path) - + if filesystem_subs: # Prefer English for sub in filesystem_subs: if sub['is_english']: return sub['path'], 'en' - + # Use first available sub = filesystem_subs[0] return sub['path'], sub['detected_language'] - + return None, None +def extract_embedded_subtitle(video_path, language_code2, media_type): + """Extract an embedded subtitle track from a video file using ffmpeg. + + Returns the path to the extracted .srt file, or None on failure. + """ + target_alpha3 = alpha3_from_alpha2(language_code2) + if not target_alpha3: + logger.error(f'Cannot convert language code {language_code2} to alpha3') + return None + + # Look up file metadata needed by parse_video_metadata + if media_type == 'series': + db_path = path_mappings.path_replace_reverse(video_path) + media = database.execute( + select(TableEpisodes.episode_file_id, TableEpisodes.file_size) + .where(TableEpisodes.path == db_path) + ).first() + if not media: + return None + data = parse_video_metadata(video_path, media.file_size, + episode_file_id=media.episode_file_id) + else: + db_path = path_mappings.path_replace_reverse_movie(video_path) + media = database.execute( + select(TableMovies.movie_file_id, TableMovies.file_size) + .where(TableMovies.path == db_path) + ).first() + if not media: + return None + data = parse_video_metadata(video_path, media.file_size, + movie_file_id=media.movie_file_id) + + if not data: + return None + + # Find the subtitle provider (ffprobe or mediainfo) + cache_provider = None + if data.get("ffprobe") and "subtitle" in data["ffprobe"]: + cache_provider = 'ffprobe' + elif data.get("mediainfo") and "subtitle" in data["mediainfo"]: + cache_provider = 'mediainfo' + if not cache_provider: + return None + + # Bitmap-based subtitle codecs that cannot be converted to SRT + bitmap_codecs = ['pgs', 'vobsub', 'dvd_subtitle', 'dvbsub', 'hdmv_pgs_subtitle', 'dvd'] + + # Find the matching subtitle stream index + track_id = 0 + found_track = None + for track in data[cache_provider]["subtitle"]: + codec = (track.get("format") or track.get("name") or "").lower() + if any(bc in codec for bc in bitmap_codecs): + track_id += 1 + continue + + if "language" not in track: + track_id += 1 + continue + + track_alpha3 = _handle_alpha3(track) + if track_alpha3 == target_alpha3: + found_track = track_id + break + track_id += 1 + + if found_track is None: + logger.debug(f'No extractable embedded subtitle found for language {language_code2} in {video_path}') + return None + + # Build output path in Bazarr's config dir so Jellyfin won't pick it up + import hashlib + extract_dir = os.path.join('/config', 'extracted_subs') + os.makedirs(extract_dir, exist_ok=True) + video_hash = hashlib.md5(video_path.encode()).hexdigest() + output_path = os.path.join(extract_dir, f"{video_hash}.{language_code2}.srt") + + # Skip extraction if already done + if os.path.exists(output_path) and os.path.getsize(output_path) > 0: + logger.debug(f'Using previously extracted subtitle: {output_path}') + return output_path + + # Extract using ffmpeg + try: + ffmpeg_path = get_binary("ffmpeg") + except Exception: + logger.error("ffmpeg binary not found, cannot extract embedded subtitles") + return None + + cmd = [ffmpeg_path, '-y', '-loglevel', 'error', + '-i', video_path, + '-map', f'0:s:{found_track}', + '-c:s', 'srt', + output_path] + + try: + result = subprocess.run(cmd, capture_output=True, text=True, timeout=120) + if result.returncode != 0: + logger.error(f'ffmpeg extraction failed for {video_path}: {result.stderr}') + if os.path.exists(output_path): + os.remove(output_path) + return None + if os.path.exists(output_path) and os.path.getsize(output_path) > 0: + # Strip Windows carriage returns (\r) that ffmpeg may produce + with open(output_path, 'r', encoding='utf-8-sig', errors='replace') as f: + content = f.read() + if '\r' in content: + with open(output_path, 'w', encoding='utf-8') as f: + f.write(content.replace('\r', '')) + logger.info(f'Extracted embedded {language_code2} subtitle to: {output_path}') + return output_path + return None + except subprocess.TimeoutExpired: + logger.error(f'ffmpeg extraction timed out for {video_path}') + if os.path.exists(output_path): + os.remove(output_path) + return None + except Exception as e: + logger.error(f'Failed to extract embedded subtitle: {e}') + if os.path.exists(output_path): + os.remove(output_path) + return None + + def scan_filesystem_for_subtitles(video_path): """Scan filesystem for .srt files next to the video file.""" ENGLISH_PATTERNS = [ diff --git a/bazarr/subtitles/tools/translate/core/translator_utils.py b/bazarr/subtitles/tools/translate/core/translator_utils.py index 2ac5700b73..e74ef31647 100644 --- a/bazarr/subtitles/tools/translate/core/translator_utils.py +++ b/bazarr/subtitles/tools/translate/core/translator_utils.py @@ -9,6 +9,7 @@ from app.config import settings from subzero.language import Language +from subliminal_patch.score import MAX_SCORES from languages.custom_lang import CustomLanguage from languages.get_languages import alpha3_from_alpha2, language_from_alpha2, language_from_alpha3 from subtitles.processing import ProcessSubtitlesResult @@ -56,10 +57,10 @@ def create_process_result(message, video_path, orig_to_lang, forced, hi, dest_sr """Create a ProcessSubtitlesResult object with common parameters.""" if media_type == 'series': prr = path_mappings.path_replace_reverse - score = int((settings.translator.default_score / 100) * 360) + score = int((settings.translator.default_score / 100) * MAX_SCORES['episode']) else: prr = path_mappings.path_replace_reverse_movie - score = int((settings.translator.default_score / 100) * 120) + score = int((settings.translator.default_score / 100) * MAX_SCORES['movie']) return ProcessSubtitlesResult( message=message, diff --git a/bazarr/subtitles/tools/translate/main.py b/bazarr/subtitles/tools/translate/main.py index 8201b47187..4c20c6fad5 100644 --- a/bazarr/subtitles/tools/translate/main.py +++ b/bazarr/subtitles/tools/translate/main.py @@ -3,10 +3,11 @@ import logging import os + from subliminal_patch.core import get_subtitle_path from subzero.language import Language -from .core.translator_utils import validate_translation_params, convert_language_codes +from .core.translator_utils import validate_translation_params, convert_language_codes, get_title from .services.translator_factory import TranslatorFactory from languages.get_languages import alpha3_from_alpha2 from app.config import settings @@ -15,13 +16,19 @@ def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, forced, hi, - media_type, sonarr_series_id, sonarr_episode_id, radarr_id, job_id=None): + media_type, sonarr_series_id, sonarr_episode_id, radarr_id, metadata, job_id=None): if not job_id: - jobs_queue.add_job_from_function(f'Translating from {from_lang.upper()} to {to_lang.upper()} using ' - f'{settings.translator.translator_type.replace("_", " ").title()}', - is_progress=True) + # Build job label with media title. Note: no local variables can be + # assigned here because add_job_from_function introspects the frame + # and re-passes all locals as kwargs on re-invocation. + jobs_queue.add_job_from_function( + (lambda t: f'Translating {t} ({from_lang.upper()} to {to_lang.upper()})' if t else + f'Translating {from_lang.upper()} to {to_lang.upper()}')( + get_title(media_type, radarr_id, sonarr_series_id, sonarr_episode_id)), + is_progress=True) return + translator_label = settings.translator.translator_type.replace("_", " ").title() try: logging.debug(f'Translation request: video={video_path}, source={source_srt_file}, from={from_lang}, to={to_lang}') @@ -30,7 +37,6 @@ def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, fo logging.debug(f'BAZARR is translating in {lang_obj} this subtitles {source_srt_file}') - # get the destination path if the subtitles are alongside the video dest_srt_file_if_alongside_video = get_subtitle_path( video_path, language=lang_obj if isinstance(lang_obj, Language) else lang_obj.subzero_language(), @@ -39,8 +45,6 @@ def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, fo hi_tag=hi ) - # get the real destination path taking into account if the user set up Bazarr to store external subtitles in - # a custom folder or relative folder dest_srt_file = get_external_subtitles_path( file=video_path, subtitle=os.path.basename(dest_srt_file_if_alongside_video) @@ -68,14 +72,29 @@ def translate_subtitles_file(video_path, source_srt_file, from_lang, to_lang, fo logging.debug(f'Created translator instance: {translator.__class__.__name__}') result = translator.translate(job_id=job_id) + if result is False: + raise RuntimeError(f'{translator.__class__.__name__} returned a failed translation result') logging.debug(f'BAZARR saved translated subtitles to {dest_srt_file}') + + from api.subtitles.subtitles import postprocess_subtitles + # Call postprocess_subtitles after translation (handles chmod, re-indexing, events) + postprocess_subtitles(dest_srt_file, video_path, media_type, metadata, sonarr_episode_id if media_type == 'series' else radarr_id) + + # Get current job name (which batch.py already set with title) and mark as done + current_name = jobs_queue.get_job_name(job_id) + if current_name and 'Translating' in current_name: + done_name = current_name.replace('Translating', 'Translated') + else: + done_name = f'Translated {from_lang.upper()} \u2192 {to_lang.upper()} using {translator_label}' + jobs_queue.update_job_name(job_id=job_id, new_job_name=done_name) return result except Exception as e: logging.error(f'Translation failed: {str(e)}', exc_info=True) - return False - - finally: - jobs_queue.update_job_name(job_id=job_id, - new_job_name=f'Translated from {from_lang.upper()} to {to_lang.upper()} using ' - f'{settings.translator.translator_type.replace("_", " ").title()}') + current_name = jobs_queue.get_job_name(job_id) + if current_name and 'Translating' in current_name: + fail_name = current_name.replace('Translating', 'Failed') + else: + fail_name = f'Failed: {from_lang.upper()} โ†’ {to_lang.upper()} using {translator_label}' + jobs_queue.update_job_name(job_id=job_id, new_job_name=fail_name) + raise diff --git a/bazarr/subtitles/tools/translate/services/auth.py b/bazarr/subtitles/tools/translate/services/auth.py new file mode 100644 index 0000000000..31cfac4468 --- /dev/null +++ b/bazarr/subtitles/tools/translate/services/auth.py @@ -0,0 +1,24 @@ +import hashlib +import hmac + +from app.config import settings + +AUTH_MESSAGE = b"subtitle-translator-auth-v1" + + +def get_translator_auth_headers(encryption_key=None): + """Build auth headers for requests to the AI Subtitle Translator. + + Computes HMAC-SHA256(encryption_key, "subtitle-translator-auth-v1") + and returns it as X-Auth-Token. Returns empty dict when no key is + configured (backwards-compatible). + """ + key = encryption_key or settings.translator.openrouter_encryption_key + if not key: + return {} + token = hmac.new( + bytes.fromhex(key), + AUTH_MESSAGE, + hashlib.sha256, + ).hexdigest() + return {"X-Auth-Token": token} diff --git a/bazarr/subtitles/tools/translate/services/encryption.py b/bazarr/subtitles/tools/translate/services/encryption.py new file mode 100644 index 0000000000..2b56ba8b52 --- /dev/null +++ b/bazarr/subtitles/tools/translate/services/encryption.py @@ -0,0 +1,36 @@ +import base64 +import os +import re + +from cryptography.hazmat.primitives.ciphers.aead import AESGCM + +_HEX_64_RE = re.compile(r'^[0-9a-fA-F]{64}$') + + +def validate_encryption_key(key_hex: str) -> bool: + """Check if key_hex is a valid 64-character hex string.""" + return bool(_HEX_64_RE.match(key_hex)) + + +def encrypt_api_key(api_key: str, encryption_key_hex: str) -> str: + """Encrypt an API key using AES-256-GCM with the shared key. + + Args: + api_key: The plaintext OpenRouter API key. + encryption_key_hex: 64-char hex string (the shared key). + + Returns: + Encrypted string in format "enc:base64data". + + Raises: + ValueError: If encryption_key_hex is not a valid 64-char hex string. + """ + if not validate_encryption_key(encryption_key_hex): + raise ValueError("Encryption key must be exactly 64 hexadecimal characters") + + key = bytes.fromhex(encryption_key_hex) + nonce = os.urandom(12) + aesgcm = AESGCM(key) + ciphertext = aesgcm.encrypt(nonce, api_key.encode("utf-8"), None) + encoded = base64.b64encode(nonce + ciphertext).decode("ascii") + return f"enc:{encoded}" diff --git a/bazarr/subtitles/tools/translate/services/gemini_translator.py b/bazarr/subtitles/tools/translate/services/gemini_translator.py index e1601f8f87..54449faed8 100644 --- a/bazarr/subtitles/tools/translate/services/gemini_translator.py +++ b/bazarr/subtitles/tools/translate/services/gemini_translator.py @@ -28,6 +28,10 @@ from ..core.translator_utils import add_translator_info, get_description, create_process_result logger = logging.getLogger(__name__) +DEFAULT_GEMINI_BATCH_SIZE = 300 +DEFAULT_GEMINI_KEY_COOLDOWN_SECONDS = 60 +_GEMINI_KEY_COOLDOWNS = {} +_GEMINI_KEY_COOLDOWNS_LOCK = threading.Lock() class SubtitleObject(typing.TypedDict): @@ -58,7 +62,9 @@ def __init__(self, source_srt_file, dest_srt_file, to_lang, media_type, sonarr_s self.orig_to_lang = orig_to_lang self.gemini_api_key = None + self.api_keys = [] self.current_api_key = None + self.current_api_index = -1 self.current_api_number = 1 self.backup_api_number = 2 self.target_language = None @@ -67,11 +73,9 @@ def __init__(self, source_srt_file, dest_srt_file, to_lang, media_type, sonarr_s self.start_line = 1 self.description = None self.model_name = "gemini-2.0-flash" - self.batch_size = 100 + self.batch_size = DEFAULT_GEMINI_BATCH_SIZE self.free_quota = True self.error_log = False - self.token_limit = 0 - self.token_count = 0 self.interrupt_flag = False self.progress_file = None self.current_progress = 0 @@ -86,17 +90,16 @@ def translate(self, job_id): logger.debug(f'BAZARR is sending subtitle file to Gemini for translation') logger.info(f"BAZARR is sending subtitle file to Gemini for translation " + self.source_srt_file) - self.gemini_api_key = settings.translator.gemini_key - self.current_api_key = self.gemini_api_key + self.api_keys = self._get_configured_api_keys() + self.current_api_key = self._select_next_api_key() + self.gemini_api_key = self.current_api_key self.target_language = language_from_alpha3(self.to_lang) self.input_file = self.source_srt_file self.output_file = self.dest_srt_file self.model_name = settings.translator.gemini_model + self.batch_size = self._get_batch_size() self.description = get_description(self.media_type, self.radarr_id, self.sonarr_series_id) - if "2.5-flash" in self.model_name or "pro" in self.model_name: - self.batch_size = 300 - if self.input_file: self.progress_file = os.path.join(os.path.dirname(self.input_file), f".{os.path.basename(self.input_file)}.progress") @@ -107,10 +110,11 @@ def translate(self, job_id): add_translator_info(self.dest_srt_file, f"# Subtitles translated with {settings.translator.gemini_model} # ") except Exception as e: jobs_queue.update_job_progress(job_id=job_id, progress_message=f'Gemini translation error: {str(e)}') + raise except Exception as e: logger.error(f'BAZARR encountered an error translating with Gemini: {str(e)}') - return False + raise else: message = (f"{language_from_alpha2(self.from_lang)} subtitles translated to " @@ -206,53 +210,113 @@ def setup_signal_handlers(self): return True return False - def _get_token_limit(self) -> int: - """ - Get the token limit for the current model. + def _get_batch_size(self) -> int: + try: + batch_size = int(settings.translator.gemini_batch_size) + except (AttributeError, TypeError, ValueError): + batch_size = DEFAULT_GEMINI_BATCH_SIZE + return max(1, batch_size) - Returns: - int: Token limit for the current model according to https://cloud.google.com/vertex-ai/generative-ai/docs/models/gemini/2-5-flash - """ - if "2.0-flash" in self.model_name: - return 7000 - elif "2.5-flash" in self.model_name or "pro" in self.model_name: - return 50000 - else: - return 7000 + def _get_configured_api_keys(self) -> List[str]: + keys = [] + seen_keys = set() + configured_keys = getattr(settings.translator, "gemini_keys", []) - def _validate_token_size(self, contents: str) -> bool: - """ - Validate the token size of the input contents. + if not isinstance(configured_keys, list): + configured_keys = [configured_keys] - Args: - contents (str): Input contents to validate + for raw_key in configured_keys: + key = str(raw_key).strip() + if key and key not in seen_keys: + keys.append(key) + seen_keys.add(key) - Returns: - bool: True if token size is valid, False otherwise - """ - return True + return keys - current_progress = 0 + @staticmethod + def _is_key_on_cooldown(api_key: str) -> bool: + now = time.time() + with _GEMINI_KEY_COOLDOWNS_LOCK: + expires_at = _GEMINI_KEY_COOLDOWNS.get(api_key) + if expires_at is None: + return False + if expires_at <= now: + _GEMINI_KEY_COOLDOWNS.pop(api_key, None) + return False + return True - def _process_batch( - self, - batch: List[SubtitleObject], # Changed from list[SubtitleObject] - translated_subtitle: List[Subtitle], # Changed from list[Subtitle] - total: int, - retry_num=3 - ): - """ - Process a batch of subtitles for translation with accurate progress tracking. + def _select_next_api_key(self): + if not self.api_keys: + self.current_api_key = None + return None - Args: - batch (List[SubtitleObject]): Batch of subtitles to translate - translated_subtitle (List[Subtitle]): List to store translated subtitles - total (int): Total number of subtitles to translate - """ + total_keys = len(self.api_keys) + start_index = self.current_api_index if self.current_api_index >= 0 else -1 - url = f"https://generativelanguage.googleapis.com/v1beta/models/{self.model_name}:generateContent?key={self.current_api_key}" + for offset in range(1, total_keys + 1): + candidate_index = (start_index + offset) % total_keys + candidate_key = self.api_keys[candidate_index] + if not self._is_key_on_cooldown(candidate_key): + self.current_api_index = candidate_index + self.current_api_key = candidate_key + return candidate_key + + self.current_api_key = None + return None + + @staticmethod + def _is_rate_limited_response(response) -> bool: + if response is None: + return False + + if response.status_code == 429: + return True + + try: + error_body = response.json().get("error", {}) + except Exception: + return False + + error_status = str(error_body.get("status", "")).upper() + error_message = str(error_body.get("message", "")).lower() + return error_status == "RESOURCE_EXHAUSTED" or "rate limit" in error_message or "quota" in error_message + + @staticmethod + def _get_retry_after_seconds(response) -> int: + if response is not None: + retry_after = response.headers.get("Retry-After") + if retry_after: + try: + return max(1, int(float(retry_after))) + except (TypeError, ValueError): + pass - payload = json.dumps({ + return DEFAULT_GEMINI_KEY_COOLDOWN_SECONDS + + @staticmethod + def _set_key_cooldown(api_key: str, cooldown_seconds: int): + with _GEMINI_KEY_COOLDOWNS_LOCK: + _GEMINI_KEY_COOLDOWNS[api_key] = time.time() + max(1, cooldown_seconds) + + def _handle_rate_limited_key(self, response): + if not self.current_api_key: + raise RuntimeError("All Gemini API keys are currently rate limited. Please wait before retrying.") + + cooldown_seconds = self._get_retry_after_seconds(response) + self._set_key_cooldown(self.current_api_key, cooldown_seconds) + + rotated_key = self._select_next_api_key() + if not rotated_key: + raise RuntimeError("All Gemini API keys are currently rate limited. Please wait before retrying.") + + if self.job_id is not None: + jobs_queue.update_job_progress( + job_id=self.job_id, + progress_message="Gemini key rate limited. Rotating to another configured key.", + ) + + def _build_generate_payload(self, batch: List[SubtitleObject]) -> dict: + return { "system_instruction": { "parts": [ { @@ -270,7 +334,29 @@ def _process_batch( ] } ] - }) + } + + current_progress = 0 + + def _process_batch( + self, + batch: List[SubtitleObject], # Changed from list[SubtitleObject] + translated_subtitle: List[Subtitle], # Changed from list[Subtitle] + total: int, + retry_num=3 + ): + """ + Process a batch of subtitles for translation with accurate progress tracking. + + Args: + batch (List[SubtitleObject]): Batch of subtitles to translate + translated_subtitle (List[Subtitle]): List to store translated subtitles + total (int): Total number of subtitles to translate + """ + + url = f"https://generativelanguage.googleapis.com/v1beta/models/{self.model_name}:generateContent?key={self.current_api_key}" + + payload = json.dumps(self._build_generate_payload(batch), ensure_ascii=False) headers = { 'Content-Type': 'application/json' } @@ -313,6 +399,12 @@ def clean_json_string(json_string): return self.current_progress except Exception as e: + response = getattr(e, "response", None) + if self._is_rate_limited_response(response): + self._handle_rate_limited_key(response) + if retry_num > 0: + return self._process_batch(batch, translated_subtitle, total, retry_num - 1) + if retry_num > 0: return self._process_batch(batch, translated_subtitle, total, retry_num - 1) else: @@ -360,8 +452,18 @@ def _dominant_strong_direction(s: str) -> str: translated_subtitle[int(line["index"])].content = line["content"] def _translate_with_gemini(self): + if not self.api_keys: + jobs_queue.update_job_progress( + job_id=self.job_id, + progress_message="Please provide at least one valid Gemini API key.", + ) + return + if not self.current_api_key: - jobs_queue.update_job_progress(job_id=self.job_id, progress_message="Please provide a valid Gemini API key.") + jobs_queue.update_job_progress( + job_id=self.job_id, + progress_message="All Gemini API keys are currently rate limited. Please wait before retrying.", + ) return if not self.target_language: @@ -372,8 +474,6 @@ def _translate_with_gemini(self): jobs_queue.update_job_progress(job_id=self.job_id, progress_message="Please provide a subtitle file.") return - self.token_limit = self._get_token_limit() - try: with open(self.input_file, "r", encoding="utf-8") as original_file: original_text = original_file.read() @@ -395,7 +495,6 @@ def _translate_with_gemini(self): i = self.start_line - 1 total = len(original_subtitle) batch = [SubtitleObject(index=str(i), content=original_subtitle[i].content)] - i += 1 # Save initial progress @@ -411,35 +510,6 @@ def _translate_with_gemini(self): continue try: - if not self._validate_token_size(json.dumps(batch, ensure_ascii=False)): - jobs_queue.update_job_progress( - job_id=self.job_id, - progress_message=f"Token size ({int(self.token_count / 0.9)}) exceeds limit (" - f"{self.token_limit}) for {self.model_name}." - ) - user_prompt = "0" - while not user_prompt.isdigit() or int(user_prompt) <= 0: - user_prompt = jobs_queue.update_job_progress( - job_id=self.job_id, - progress_message=f"Please enter a new batch size (current: {self.batch_size}): " - ) - if user_prompt.isdigit() and int(user_prompt) > 0: - new_batch_size = int(user_prompt) - decrement = self.batch_size - new_batch_size - if decrement > 0: - for _ in range(decrement): - i -= 1 - batch.pop() - self.batch_size = new_batch_size - jobs_queue.update_job_progress(job_id=self.job_id, - progress_message=f"Batch size updated to " - f"{self.batch_size}.") - else: - jobs_queue.update_job_progress(job_id=self.job_id, - progress_message="Invalid input. Batch size must" - " be a positive integer.") - continue - start_time = time.time() self._process_batch(batch, translated_subtitle, total) end_time = time.time() @@ -456,7 +526,8 @@ def _translate_with_gemini(self): raise e # Check if we exited the loop due to an interrupt - jobs_queue.update_job_progress(job_id=self.job_id, progress_value=total) + jobs_queue.update_job_progress(job_id=self.job_id, progress_value=total, + progress_message=self.source_srt_file) if self.interrupt_flag: # File will be automatically closed by the with statement self._clear_progress() @@ -472,4 +543,10 @@ def _translate_with_gemini(self): jobs_queue.update_job_progress(job_id=self.job_id, progress_value=total, progress_message=f'Gemini translation failed: {str(e)}') self._clear_progress() + if self.output_file and os.path.exists(self.output_file): + try: + if os.path.getsize(self.output_file) == 0: + os.remove(self.output_file) + except OSError: + pass raise e diff --git a/bazarr/subtitles/tools/translate/services/google_translator.py b/bazarr/subtitles/tools/translate/services/google_translator.py index eea90dfa01..f121f88caf 100644 --- a/bazarr/subtitles/tools/translate/services/google_translator.py +++ b/bazarr/subtitles/tools/translate/services/google_translator.py @@ -95,7 +95,7 @@ def translate_line(line_id, subtitle_line): jobs_queue.update_job_progress(job_id=job_id, progress_message=f'Translation failed: Unable to translate ' f'malformed subtitles for {self.source_srt_file}') - return False + raise try: subs.save(self.dest_srt_file) @@ -121,7 +121,7 @@ def translate_line(line_id, subtitle_line): logger.error(f'BAZARR encountered an error during translation: {str(e)}') jobs_queue.update_job_progress(job_id=job_id, progress_message=f'Google translation failed: {str(e)}') - return False + raise @retry(exceptions=(TooManyRequests, RequestError), tries=6, delay=1, backoff=2, jitter=(0, 1)) def _translate_text(self, text, job_id): diff --git a/bazarr/subtitles/tools/translate/services/lingarr_translator.py b/bazarr/subtitles/tools/translate/services/lingarr_translator.py index 646b6e402c..276ee124f6 100644 --- a/bazarr/subtitles/tools/translate/services/lingarr_translator.py +++ b/bazarr/subtitles/tools/translate/services/lingarr_translator.py @@ -64,7 +64,7 @@ def translate(self, job_id=None): logger.error(f'Translation failed for {self.source_srt_file}') jobs_queue.update_job_progress(job_id=job_id, progress_message=f'Translation failed for {self.source_srt_file}') - return False + raise RuntimeError(f'Translation failed for {self.source_srt_file}') logger.debug(f'BAZARR saving Lingarr translated subtitles to {self.dest_srt_file}') translation_map = {} @@ -108,7 +108,7 @@ def translate(self, job_id=None): except Exception as e: logger.error(f'BAZARR encountered an error during Lingarr translation: {str(e)}') jobs_queue.update_job_progress(job_id=job_id, progress_message=f'Lingarr translation failed: {str(e)}') - return False + raise @retry(exceptions=(TooManyRequests, RequestError, requests.exceptions.RequestException), tries=3, delay=1, backoff=2, jitter=(0, 1)) diff --git a/bazarr/subtitles/tools/translate/services/openrouter_translator.py b/bazarr/subtitles/tools/translate/services/openrouter_translator.py index 7d897bfcf3..82b107ea31 100644 --- a/bazarr/subtitles/tools/translate/services/openrouter_translator.py +++ b/bazarr/subtitles/tools/translate/services/openrouter_translator.py @@ -14,8 +14,10 @@ from radarr.history import history_log_movie from sonarr.history import history_log from app.event_handler import show_progress, hide_progress, show_message +from app.jobs_queue import jobs_queue from ..core.translator_utils import add_translator_info, create_process_result, get_title +from .auth import get_translator_auth_headers logger = logging.getLogger(__name__) @@ -51,27 +53,30 @@ def __init__(self, source_srt_file, dest_srt_file, lang_obj, to_lang, from_lang, def _build_reasoning_config(self): """ Build reasoning configuration based on Bazarr settings. - Maps effort levels to max_tokens for the AI Subtitle Translator service. + Sends effort level directly to the AI Subtitle Translator service. """ reasoning_mode = getattr(settings.translator, 'openrouter_reasoning', 'disabled') - + if reasoning_mode == 'disabled': return None - - # Map effort levels to max_tokens for reasoning - effort_to_tokens = { - 'low': 1000, # Minimal thinking - 'medium': 2000, # Default balanced - 'high': 4000, # Extended thinking - } - - max_tokens = effort_to_tokens.get(reasoning_mode, 2000) - + return { - 'enabled': True, - 'maxTokens': max_tokens, + 'effort': reasoning_mode, } + def _get_api_key_value(self): + """Get the API key, encrypted if an encryption key is configured.""" + api_key = settings.translator.openrouter_api_key + encryption_key = settings.translator.openrouter_encryption_key + if encryption_key: + try: + from .encryption import encrypt_api_key + api_key = encrypt_api_key(api_key, encryption_key) + except ValueError as e: + logger.error(f'Invalid encryption key: {e}') + raise ValueError("Invalid encryption key format. Check your encryption key in Settings.") + return api_key + def translate(self, job_id=None): try: subs = pysubs2.load(self.source_srt_file, encoding='utf-8') @@ -85,7 +90,7 @@ def translate(self, job_id=None): logger.debug(f'Starting AI translation for {self.source_srt_file}') # Submit job and poll for completion - translated_lines = self._submit_and_poll(lines_list) + translated_lines = self._submit_and_poll(lines_list, bazarr_job_id=job_id) if translated_lines is None: logger.error(f'Translation failed for {self.source_srt_file}') @@ -132,7 +137,7 @@ def translate(self, job_id=None): hide_progress(id=f'translate_progress_{self.dest_srt_file}') return False - def _submit_and_poll(self, lines_list: List[str]) -> Optional[List[Dict[str, Any]]]: + def _submit_and_poll(self, lines_list: List[str], bazarr_job_id=None) -> Optional[List[Dict[str, Any]]]: """Submit translation job and poll for completion with progress updates""" try: # Prepare language codes @@ -146,6 +151,10 @@ def _submit_and_poll(self, lines_list: List[str]) -> Optional[List[Dict[str, Any source_lang = self.language_code_convert_dict.get(source_lang, source_lang) target_lang = self.language_code_convert_dict.get(target_lang, target_lang) + # Resolve alpha2 codes to full language names for the AI translator prompt + source_lang = language_from_alpha2(source_lang) or source_lang + target_lang = language_from_alpha2(target_lang) or target_lang + logger.debug(f'BAZARR translation language codes: from_lang={self.from_lang}, to_lang={self.to_lang}, ' f'orig_to_lang={self.orig_to_lang}, final source={source_lang}, final target={target_lang}') @@ -174,10 +183,11 @@ def _submit_and_poll(self, lines_list: List[str]) -> Optional[List[Dict[str, Any "lines": lines_payload, # Add configuration from Bazarr settings "config": { - "apiKey": settings.translator.openrouter_api_key, + "apiKey": self._get_api_key_value(), "model": settings.translator.openrouter_model, "temperature": settings.translator.openrouter_temperature, "maxConcurrentJobs": settings.translator.openrouter_max_concurrent, + "parallelBatches": settings.translator.openrouter_parallel_batches, "reasoning": self._build_reasoning_config(), } } @@ -189,7 +199,7 @@ def _submit_and_poll(self, lines_list: List[str]) -> Optional[List[Dict[str, Any submit_response = requests.post( f"{base_url}/api/v1/jobs/translate/content", json=payload, - headers={"Content-Type": "application/json"}, + headers={"Content-Type": "application/json", **get_translator_auth_headers()}, timeout=30 ) @@ -207,7 +217,7 @@ def _submit_and_poll(self, lines_list: List[str]) -> Optional[List[Dict[str, Any logger.debug(f'BAZARR translation job submitted: {job_id}') # Poll for completion - return self._poll_job(base_url, job_id, len(lines_payload)) + return self._poll_job(base_url, job_id, len(lines_payload), bazarr_job_id=bazarr_job_id) except requests.exceptions.Timeout: logger.error('AI Subtitle Translator request timed out') @@ -219,7 +229,7 @@ def _submit_and_poll(self, lines_list: List[str]) -> Optional[List[Dict[str, Any logger.error(f'AI Subtitle Translator error: {str(e)}') return None - def _poll_job(self, base_url: str, job_id: str, total_lines: int) -> Optional[Any]: + def _poll_job(self, base_url: str, job_id: str, total_lines: int, bazarr_job_id=None) -> Optional[Any]: """Poll job status until completion""" poll_interval = 2 # seconds max_wait_time = 1800 # 30 minutes @@ -229,6 +239,7 @@ def _poll_job(self, base_url: str, job_id: str, total_lines: int) -> Optional[An try: status_response = requests.get( f"{base_url}/api/v1/jobs/{job_id}", + headers=get_translator_auth_headers(), timeout=10 ) @@ -252,6 +263,16 @@ def _poll_job(self, base_url: str, job_id: str, total_lines: int) -> Optional[An count=100 ) + # Sync progress to bazarr jobs queue (for NotificationDrawer) + if bazarr_job_id: + model_used = job_status.get("model_used", settings.translator.openrouter_model or "") + jobs_queue.update_job_progress( + job_id=bazarr_job_id, + progress_value=progress, + progress_max=100, + progress_message=f'{message} [{model_used}]' if model_used else message + ) + if status == "completed": hide_progress(id=f'translate_progress_{self.dest_srt_file}') result = job_status.get("result") @@ -273,6 +294,13 @@ def _poll_job(self, base_url: str, job_id: str, total_lines: int) -> Optional[An show_message(f"Translation failed: {error}") return None + elif status == "partial": + hide_progress(id=f'translate_progress_{self.dest_srt_file}') + error = job_status.get("error", message or "Partial translation") + logger.error(f"Translation partially failed: {error}") + show_message(f"Translation failed (partial): {error}") + return None + elif status == "cancelled": hide_progress(id=f'translate_progress_{self.dest_srt_file}') logger.info("Translation job was cancelled") @@ -301,7 +329,7 @@ def _translate_sync(self, lines_list: List[str], payload: Dict[str, Any]) -> Opt response = requests.post( f"{base_url}/api/v1/translate/content", json=payload, - headers={"Content-Type": "application/json"}, + headers={"Content-Type": "application/json", **get_translator_auth_headers()}, timeout=1800 ) diff --git a/bazarr/subtitles/upgrade.py b/bazarr/subtitles/upgrade.py index 88a095d748..7613fecfd8 100644 --- a/bazarr/subtitles/upgrade.py +++ b/bazarr/subtitles/upgrade.py @@ -24,25 +24,50 @@ from app.event_handler import event_stream -def upgrade_subtitles(): +def upgrade_subtitles(wait_for_completion=False): use_sonarr = settings.general.use_sonarr use_radarr = settings.general.use_radarr if use_sonarr: - upgrade_episodes_subtitles() + upgrade_episodes_subtitles(wait_for_completion=wait_for_completion) if use_radarr: - upgrade_movies_subtitles() + upgrade_movies_subtitles(wait_for_completion=wait_for_completion) logging.info('BAZARR Finished searching for Subtitles to upgrade. Check History for more information.') -def upgrade_episodes_subtitles(job_id=None): +def upgrade_episodes_subtitles(job_id=None, sonarr_series_ids=None, wait_for_completion=False): if not job_id: - jobs_queue.add_job_from_function("Trying to upgrade episodes subtitles", is_progress=True) + jobs_queue.add_job_from_function("Trying to upgrade episodes subtitles", is_progress=True, + wait_for_completion=wait_for_completion) return episodes_to_upgrade = get_upgradable_episode_subtitles() + + query = select(TableHistory.id, + TableShows.title.label('seriesTitle'), + TableEpisodes.season, + TableEpisodes.episode, + TableEpisodes.title, + TableHistory.language, + TableEpisodes.audio_language, + TableHistory.video_path, + TableEpisodes.sceneName, + TableHistory.score, + TableHistory.sonarrEpisodeId, + TableHistory.sonarrSeriesId, + TableHistory.subtitles_path, + TableEpisodes.path, + TableShows.profileId, + TableEpisodes.subtitles.label('external_subtitles')) \ + .select_from(TableHistory) \ + .join(TableShows, onclause=TableHistory.sonarrSeriesId == TableShows.sonarrSeriesId) \ + .join(TableEpisodes, onclause=TableHistory.sonarrEpisodeId == TableEpisodes.sonarrEpisodeId) + + if sonarr_series_ids: + query = query.where(TableHistory.sonarrSeriesId.in_(sonarr_series_ids)) + episodes_data = [{ 'id': x.id, 'seriesTitle': x.seriesTitle, @@ -60,26 +85,7 @@ def upgrade_episodes_subtitles(job_id=None): 'path': x.path, 'profileId': x.profileId, 'external_subtitles': [y[1] for y in ast.literal_eval(x.external_subtitles) if y[1]], - } for x in database.execute( - select(TableHistory.id, - TableShows.title.label('seriesTitle'), - TableEpisodes.season, - TableEpisodes.episode, - TableEpisodes.title, - TableHistory.language, - TableEpisodes.audio_language, - TableHistory.video_path, - TableEpisodes.sceneName, - TableHistory.score, - TableHistory.sonarrEpisodeId, - TableHistory.sonarrSeriesId, - TableHistory.subtitles_path, - TableEpisodes.path, - TableShows.profileId, - TableEpisodes.subtitles.label('external_subtitles')) - .select_from(TableHistory) - .join(TableShows, onclause=TableHistory.sonarrSeriesId == TableShows.sonarrSeriesId) - .join(TableEpisodes, onclause=TableHistory.sonarrEpisodeId == TableEpisodes.sonarrEpisodeId)) + } for x in database.execute(query) .all() if _language_still_desired(x.language, x.profileId) and x.video_path == x.path ] @@ -87,7 +93,14 @@ def upgrade_episodes_subtitles(job_id=None): for item in episodes_data: # do not consider subtitles that do not exist on disk anymore if item['subtitles_path'] not in item['external_subtitles']: - continue + current_sub = _find_current_subtitle_for_language(item['language'], item['external_subtitles']) + if current_sub: + logging.debug(f"Upgrade candidate {item['id']} ({item['seriesTitle']} S{item['season']:02d}E" + f"{item['episode']:02d}): history path no longer on disk, using current subtitle " + f"for same language ({current_sub})") + item['subtitles_path'] = current_sub + else: + continue # Mark upgradable and get original_id item.update({'original_id': episodes_to_upgrade.get(item['id'])}) @@ -134,7 +147,7 @@ def upgrade_episodes_subtitles(job_id=None): episode['seriesTitle'], 'series', episode['profileId'], - forced_minimum_score=int(episode['score']) + 1, + forced_minimum_score=int(episode['score'] or 0) + 1, is_upgrade=True, previous_subtitles_to_delete=path_mappings.path_replace( episode['subtitles_path']), @@ -153,27 +166,15 @@ def upgrade_episodes_subtitles(job_id=None): jobs_queue.update_job_name(job_id=job_id, new_job_name='Tried to upgrade episodes subtitles') -def upgrade_movies_subtitles(job_id=None): +def upgrade_movies_subtitles(job_id=None, radarr_ids=None, wait_for_completion=False): if not job_id: - jobs_queue.add_job_from_function("Trying to upgrade movies subtitles", is_progress=True) + jobs_queue.add_job_from_function("Trying to upgrade movies subtitles", is_progress=True, + wait_for_completion=wait_for_completion) return movies_to_upgrade = get_upgradable_movies_subtitles() - movies_data = [{ - 'id': x.id, - 'title': x.title, - 'language': x.language, - 'audio_language': x.audio_language, - 'video_path': x.video_path, - 'sceneName': x.sceneName, - 'score': x.score, - 'radarrId': x.radarrId, - 'path': x.path, - 'profileId': x.profileId, - 'subtitles_path': x.subtitles_path, - 'external_subtitles': [y[1] for y in ast.literal_eval(x.external_subtitles) if y[1]], - } for x in database.execute( - select(TableHistoryMovie.id, + + query = select(TableHistoryMovie.id, TableMovies.title, TableHistoryMovie.language, TableMovies.audio_language, @@ -184,17 +185,55 @@ def upgrade_movies_subtitles(job_id=None): TableHistoryMovie.subtitles_path, TableMovies.path, TableMovies.profileId, - TableMovies.subtitles.label('external_subtitles')) - .select_from(TableHistoryMovie) - .join(TableMovies, onclause=TableHistoryMovie.radarrId == TableMovies.radarrId)) - .all() if _language_still_desired(x.language, x.profileId) and - x.video_path == x.path - ] + TableMovies.subtitles.label('external_subtitles')) \ + .select_from(TableHistoryMovie) \ + .join(TableMovies, onclause=TableHistoryMovie.radarrId == TableMovies.radarrId) + + if radarr_ids: + query = query.where(TableHistoryMovie.radarrId.in_(radarr_ids)) + + all_rows = database.execute(query).all() + movies_data = [] + for x in all_rows: + if not _language_still_desired(x.language, x.profileId): + if x.id in movies_to_upgrade: + logging.debug(f"Upgrade candidate {x.id} ({x.title}) dropped: language {x.language} no longer desired " + f"in profile {x.profileId}") + continue + if x.video_path != x.path: + if x.id in movies_to_upgrade: + logging.debug(f"Upgrade candidate {x.id} ({x.title}) dropped: video_path mismatch " + f"(history={x.video_path} != current={x.path})") + continue + movies_data.append({ + 'id': x.id, + 'title': x.title, + 'language': x.language, + 'audio_language': x.audio_language, + 'video_path': x.video_path, + 'sceneName': x.sceneName, + 'score': x.score, + 'radarrId': x.radarrId, + 'path': x.path, + 'profileId': x.profileId, + 'subtitles_path': x.subtitles_path, + 'external_subtitles': [y[1] for y in ast.literal_eval(x.external_subtitles) if y[1]], + }) for item in movies_data: # do not consider subtitles that do not exist on disk anymore if item['subtitles_path'] not in item['external_subtitles']: - continue + # try to find a current subtitle for the same language (file may have been renamed/re-downloaded) + current_sub = _find_current_subtitle_for_language(item['language'], item['external_subtitles']) + if current_sub: + logging.debug(f"Upgrade candidate {item['id']} ({item['title']}): history path no longer on disk, " + f"using current subtitle for same language ({current_sub})") + item['subtitles_path'] = current_sub + else: + if item['id'] in movies_to_upgrade: + logging.debug(f"Upgrade candidate {item['id']} ({item['title']}) dropped: no subtitle for language " + f"{item['language']} found on disk") + continue # Mark upgradable and get original_id item.update({'original_id': movies_to_upgrade.get(item['id'])}) @@ -239,7 +278,7 @@ def upgrade_movies_subtitles(job_id=None): movie['title'], 'movie', movie['profileId'], - forced_minimum_score=int(movie['score']) + 1, + forced_minimum_score=int(movie['score'] or 0) + 1, is_upgrade=True, previous_subtitles_to_delete=path_mappings.path_replace_movie( movie['subtitles_path']), @@ -269,6 +308,33 @@ def get_queries_condition_parameters(): return [minimum_timestamp, query_actions] +def _find_current_subtitle_for_language(language_string, external_subtitles): + """Find a current subtitle file on disk that matches the language from history. + + When a subtitle was re-downloaded or renamed (e.g. .hu.hi.srt -> .hu.srt), + the history still references the old path. This finds the current file for + the same language so upgrades can still proceed. + """ + lang_code = language_string.split(':')[0] + is_hi = language_string.endswith(':hi') + is_forced = language_string.endswith(':forced') + + for sub_path in external_subtitles: + sub_lower = sub_path.lower() + # Check if language code is in the filename + if f'.{lang_code.lower()}.' not in sub_lower and not sub_lower.endswith(f'.{lang_code.lower()}'): + continue + # Match HI/forced flags + has_hi = '.hi.' in sub_lower or sub_lower.endswith('.hi.srt') + has_forced = '.forced.' in sub_lower or sub_lower.endswith('.forced.srt') + if is_hi == has_hi and is_forced == has_forced: + return sub_path + # If we wanted HI but only non-HI exists (or vice versa), still a candidate + if not is_forced and not has_forced and lang_code.lower() in sub_lower: + return sub_path + return None + + def parse_language_string(language_string): if language_string.endswith('forced'): language = language_string.split(':')[0] @@ -307,7 +373,7 @@ def get_upgradable_episode_subtitles(history_id_list=None): upgradable_episodes_conditions = [(TableHistory.action.in_(query_actions)), (TableHistory.timestamp > minimum_timestamp), or_(and_(TableHistory.score.is_(None), TableHistory.action == 6), - (TableHistory.score < 357))] + (TableHistory.score < TableHistory.score_out_of - 3))] upgradable_episodes_conditions += get_exclusion_clause('series') subtitles_to_upgrade = database.execute( select(TableHistory.id, @@ -374,7 +440,7 @@ def get_upgradable_movies_subtitles(history_id_list=None): upgradable_movies_conditions = [(TableHistoryMovie.action.in_(query_actions)), (TableHistoryMovie.timestamp > minimum_timestamp), or_(and_(TableHistoryMovie.score.is_(None), TableHistoryMovie.action == 6), - (TableHistoryMovie.score < 117))] + (TableHistoryMovie.score < TableHistoryMovie.score_out_of - 3))] upgradable_movies_conditions += get_exclusion_clause('movie') subtitles_to_upgrade = database.execute( select(TableHistoryMovie.id, diff --git a/bazarr/subtitles/upload.py b/bazarr/subtitles/upload.py index 433bfe4e60..aa2f71ced5 100644 --- a/bazarr/subtitles/upload.py +++ b/bazarr/subtitles/upload.py @@ -8,6 +8,7 @@ from subzero.language import Language from subliminal_patch.core import save_subtitles from subliminal_patch.subtitle import Subtitle +from subliminal_patch.score import MAX_SCORES from pysubs2.formats import get_format_identifier from languages.get_languages import language_from_alpha3, alpha2_from_alpha3, alpha3_from_alpha2 @@ -197,14 +198,13 @@ def manual_upload_subtitle(path, language, forced, hi, media_type, subtitle, fil result = result[0] provider = "manual" if media_type == 'series': - score = 360 - history_log(4, sonarrSeriesId, sonarrEpisodeId, result, fake_provider=provider, fake_score=score) + history_log(4, sonarrSeriesId, sonarrEpisodeId, result, fake_provider=provider, + fake_score=MAX_SCORES['episode']) if not settings.general.dont_notify_manual_actions: send_notifications(sonarrSeriesId, sonarrEpisodeId, result.message) store_subtitles(result.path, path) else: - score = 120 - history_log_movie(4, radarrId, result, fake_provider=provider, fake_score=score) + history_log_movie(4, radarrId, result, fake_provider=provider, fake_score=MAX_SCORES['movie']) if not settings.general.dont_notify_manual_actions: send_notifications_movie(radarrId, result.message) store_subtitles_movie(result.path, path) diff --git a/bazarr/subtitles/utils.py b/bazarr/subtitles/utils.py index 585c264db9..b2da03e09e 100644 --- a/bazarr/subtitles/utils.py +++ b/bazarr/subtitles/utils.py @@ -8,11 +8,11 @@ from subzero.language import Language from subzero.video import parse_video from guessit.jsonutils import GuessitEncoder +from subliminal_patch.score import MAX_SCORES, DEFAULT_SCORES from app.config import settings from languages.custom_lang import CustomLanguage from app.database import get_profiles_list -from subtitles.tools.score import movie_score, series_score from .refiners import registered as registered_refiners @@ -72,11 +72,19 @@ def _get_lang_obj(alpha3): def _get_scores(media_type, min_movie=None, min_ep=None): series = "series" == media_type - handler = series_score if series else movie_score - min_movie = min_movie or (60 * 100 / handler.max_score) - min_ep = min_ep or (240 * 100 / handler.max_score) - min_score_ = int(min_ep if series else min_movie) - return handler.get_scores(min_score_) + handler = DEFAULT_SCORES['episode'] if series else DEFAULT_SCORES['movie'] + + max_score = MAX_SCORES['episode' if series else 'movie'] + + min_movie = min_movie or (max_score / 2) + min_ep = min_ep or (2/3 * max_score) + min_score = int(min_ep if series else min_movie) + + return ( + max_score * min_score / 100, + max_score, + handler.keys(), + ) def get_ban_list(profile_id): diff --git a/bazarr/subtitles/wanted/movies.py b/bazarr/subtitles/wanted/movies.py index b00e87aa55..b41e538be4 100644 --- a/bazarr/subtitles/wanted/movies.py +++ b/bazarr/subtitles/wanted/movies.py @@ -97,9 +97,10 @@ def wanted_download_subtitles_movie(radarr_id, job_id=None): logging.info("BAZARR All providers are throttled") -def wanted_search_missing_subtitles_movies(job_id=None): +def wanted_search_missing_subtitles_movies(job_id=None, wait_for_completion=False): if not job_id: - jobs_queue.add_job_from_function("Searching for missing movies subtitles", is_progress=True) + jobs_queue.add_job_from_function("Searching for missing movies subtitles", is_progress=True, + wait_for_completion=wait_for_completion) return conditions = [(TableMovies.missing_subtitles.is_not(None)), @@ -119,6 +120,7 @@ def wanted_search_missing_subtitles_movies(job_id=None): if count_movies == 0: jobs_queue.update_job_progress(job_id=job_id, progress_value='max') + throttled = False for i, movie in enumerate(movies, start=1): jobs_queue.update_job_progress(job_id=job_id, progress_value=i, progress_message=movie.title) @@ -130,7 +132,11 @@ def wanted_search_missing_subtitles_movies(job_id=None): jobs_queue.update_job_progress(job_id=job_id, progress_value=i, progress_max=count_movies) else: logging.info("BAZARR All providers are throttled") + throttled = True break + outcome_msg = ("All providers throttled" if throttled + else "Search completed") + jobs_queue.update_job_progress(job_id=job_id, progress_message=outcome_msg) jobs_queue.update_job_name(job_id=job_id, new_job_name="Searched for missing movies subtitles") logging.info('BAZARR Finished searching for missing Movies Subtitles. Check History for more information.') diff --git a/bazarr/subtitles/wanted/series.py b/bazarr/subtitles/wanted/series.py index d6deafdad8..359678cf1e 100644 --- a/bazarr/subtitles/wanted/series.py +++ b/bazarr/subtitles/wanted/series.py @@ -4,6 +4,7 @@ import ast import logging import operator +import gc from functools import reduce @@ -102,9 +103,10 @@ def wanted_download_subtitles(sonarr_episode_id, job_id=None): logging.info("BAZARR All providers are throttled") -def wanted_search_missing_subtitles_series(job_id=None): +def wanted_search_missing_subtitles_series(job_id=None, wait_for_completion=False): if not job_id: - jobs_queue.add_job_from_function("Searching for missing series subtitles", is_progress=True) + jobs_queue.add_job_from_function("Searching for missing series subtitles", is_progress=True, + wait_for_completion=wait_for_completion) return conditions = [(TableEpisodes.missing_subtitles.is_not(None)), @@ -131,6 +133,7 @@ def wanted_search_missing_subtitles_series(job_id=None): if count_episodes == 0: jobs_queue.update_job_progress(job_id=job_id, progress_value='max') + throttled = False for i, episode in enumerate(episodes, start=1): jobs_queue.update_job_progress(job_id=job_id, progress_value=i, progress_message=f'{episode.title} - S{episode.season:02d}E{episode.episode:02d}' @@ -144,7 +147,13 @@ def wanted_search_missing_subtitles_series(job_id=None): jobs_queue.update_job_progress(job_id=job_id, progress_value=i, progress_max=count_episodes) else: logging.info("BAZARR All providers are throttled") + throttled = True break + outcome_msg = ("All providers throttled" if throttled + else "Search completed") + jobs_queue.update_job_progress(job_id=job_id, progress_message=outcome_msg) jobs_queue.update_job_name(job_id=job_id, new_job_name="Searched for missing series subtitles") logging.info('BAZARR Finished searching for missing Series Subtitles. Check History for more information.') + + gc.collect() diff --git a/bazarr/utilities/backup.py b/bazarr/utilities/backup.py index 75368ebfbd..0ce9977286 100644 --- a/bazarr/utilities/backup.py +++ b/bazarr/utilities/backup.py @@ -47,9 +47,10 @@ def get_backup_files(fullpath=True): } for x in file_list] -def backup_to_zip(job_id=None): +def backup_to_zip(job_id=None, wait_for_completion=False): if not job_id: - jobs_queue.add_job_from_function("Backing up Database and Configuration File", is_progress=False) + jobs_queue.add_job_from_function("Backing up Database and Configuration File", is_progress=False, + wait_for_completion=wait_for_completion) return now = datetime.now() diff --git a/bazarr/utilities/binaries.json b/bazarr/utilities/binaries.json index 4255cd005b..6f072d410e 100644 --- a/bazarr/utilities/binaries.json +++ b/bazarr/utilities/binaries.json @@ -5,7 +5,7 @@ "directory": "unrar", "name": "unrar", "checksum": "be7a08a75ffb1dcc5f8c6b16a75e822c", - "url": "https://github.com/morpheus65535/bazarr-binaries/raw/master/bin/Linux/aarch64/unrar/unrar" + "url": "https://github.com/LavX/bazarr-binaries/raw/master/bin/Linux/aarch64/unrar/unrar" }, { "system": "Linux", @@ -13,7 +13,7 @@ "directory": "unrar", "name": "unrar", "checksum": "07a6371cc7db8493352739ce26b19ea1", - "url": "https://github.com/morpheus65535/bazarr-binaries/raw/master/bin/Linux/armv5tel/unrar/unrar" + "url": "https://github.com/LavX/bazarr-binaries/raw/master/bin/Linux/armv5tel/unrar/unrar" }, { "system": "Linux", @@ -21,7 +21,7 @@ "directory": "ffmpeg", "name": "ffmpeg", "checksum": "43910b9df223a772402830461a80e9a1", - "url": "https://github.com/morpheus65535/bazarr-binaries/raw/master/bin/Linux/i386/ffmpeg/ffmpeg" + "url": "https://github.com/LavX/bazarr-binaries/raw/master/bin/Linux/i386/ffmpeg/ffmpeg" }, { "system": "Linux", @@ -29,7 +29,7 @@ "directory": "ffmpeg", "name": "ffprobe", "checksum": "24ccbd651630c562fbaf17e30a18ce38", - "url": "https://github.com/morpheus65535/bazarr-binaries/raw/master/bin/Linux/i386/ffmpeg/ffprobe" + "url": "https://github.com/LavX/bazarr-binaries/raw/master/bin/Linux/i386/ffmpeg/ffprobe" }, { "system": "Linux", @@ -37,7 +37,7 @@ "directory": "unrar", "name": "unrar", "checksum": "f03ae1d4abf871b95142a7248a6cfa3a", - "url": "https://github.com/morpheus65535/bazarr-binaries/raw/master/bin/Linux/i386/unrar/unrar" + "url": "https://github.com/LavX/bazarr-binaries/raw/master/bin/Linux/i386/unrar/unrar" }, { "system": "Linux", @@ -45,7 +45,7 @@ "directory": "ffmpeg", "name": "ffmpeg", "checksum": "9fd68f7b80cd1177b94a71455b288131", - "url": "https://github.com/morpheus65535/bazarr-binaries/raw/master/bin/Linux/x86_64/ffmpeg/ffmpeg" + "url": "https://github.com/LavX/bazarr-binaries/raw/master/bin/Linux/x86_64/ffmpeg/ffmpeg" }, { "system": "Linux", @@ -53,7 +53,7 @@ "directory": "ffmpeg", "name": "ffprobe", "checksum": "29fdc34d9c7ad07c8f75e83d8fb5965b", - "url": "https://github.com/morpheus65535/bazarr-binaries/raw/master/bin/Linux/x86_64/ffmpeg/ffprobe" + "url": "https://github.com/LavX/bazarr-binaries/raw/master/bin/Linux/x86_64/ffmpeg/ffprobe" }, { "system": "Linux", @@ -61,7 +61,7 @@ "directory": "unrar", "name": "unrar", "checksum": "2a63e80d50c039e1fba01de7dcfa6432", - "url": "https://github.com/morpheus65535/bazarr-binaries/raw/master/bin/Linux/x86_64/unrar/unrar" + "url": "https://github.com/LavX/bazarr-binaries/raw/master/bin/Linux/x86_64/unrar/unrar" }, { "system": "MacOSX", @@ -69,7 +69,7 @@ "directory": "ffmpeg", "name": "ffmpeg", "checksum": "af1723314ba5ca9c70ef35dc8b5c2260", - "url": "https://github.com/morpheus65535/bazarr-binaries/raw/master/bin/MacOSX/i386/ffmpeg/ffmpeg" + "url": "https://github.com/LavX/bazarr-binaries/raw/master/bin/MacOSX/i386/ffmpeg/ffmpeg" }, { "system": "MacOSX", @@ -77,7 +77,7 @@ "directory": "ffmpeg", "name": "ffprobe", "checksum": "d81468cebd6630450d2f5a17720b4504", - "url": "https://github.com/morpheus65535/bazarr-binaries/raw/master/bin/MacOSX/i386/ffmpeg/ffprobe" + "url": "https://github.com/LavX/bazarr-binaries/raw/master/bin/MacOSX/i386/ffmpeg/ffprobe" }, { "system": "MacOSX", @@ -85,7 +85,7 @@ "directory": "unrar", "name": "unrar", "checksum": "47de1fed0a5d4f7bac4d8f7557926398", - "url": "https://github.com/morpheus65535/bazarr-binaries/raw/master/bin/MacOSX/i386/unrar/unrar" + "url": "https://github.com/LavX/bazarr-binaries/raw/master/bin/MacOSX/i386/unrar/unrar" }, { "system": "MacOSX", @@ -93,7 +93,7 @@ "directory": "ffmpeg", "name": "ffmpeg", "checksum": "af1723314ba5ca9c70ef35dc8b5c2260", - "url": "https://github.com/morpheus65535/bazarr-binaries/raw/master/bin/MacOSX/x86_64/ffmpeg/ffmpeg" + "url": "https://github.com/LavX/bazarr-binaries/raw/master/bin/MacOSX/x86_64/ffmpeg/ffmpeg" }, { "system": "MacOSX", @@ -101,7 +101,7 @@ "directory": "ffmpeg", "name": "ffprobe", "checksum": "d81468cebd6630450d2f5a17720b4504", - "url": "https://github.com/morpheus65535/bazarr-binaries/raw/master/bin/MacOSX/x86_64/ffmpeg/ffprobe" + "url": "https://github.com/LavX/bazarr-binaries/raw/master/bin/MacOSX/x86_64/ffmpeg/ffprobe" }, { "system": "MacOSX", @@ -109,7 +109,7 @@ "directory": "unrar", "name": "unrar", "checksum": "8d5b3d5d6be2c14b74b02767430ade9c", - "url": "https://github.com/morpheus65535/bazarr-binaries/raw/master/bin/MacOSX/x86_64/unrar/unrar" + "url": "https://github.com/LavX/bazarr-binaries/raw/master/bin/MacOSX/x86_64/unrar/unrar" }, { "system": "MacOSX", @@ -117,7 +117,7 @@ "directory": "ffmpeg", "name": "ffmpeg", "checksum": "af1723314ba5ca9c70ef35dc8b5c2260", - "url": "https://github.com/morpheus65535/bazarr-binaries/raw/master/bin/MacOSX/x86_64/ffmpeg/ffmpeg" + "url": "https://github.com/LavX/bazarr-binaries/raw/master/bin/MacOSX/x86_64/ffmpeg/ffmpeg" }, { "system": "MacOSX", @@ -125,7 +125,7 @@ "directory": "ffmpeg", "name": "ffprobe", "checksum": "d81468cebd6630450d2f5a17720b4504", - "url": "https://github.com/morpheus65535/bazarr-binaries/raw/master/bin/MacOSX/x86_64/ffmpeg/ffprobe" + "url": "https://github.com/LavX/bazarr-binaries/raw/master/bin/MacOSX/x86_64/ffmpeg/ffprobe" }, { "system": "MacOSX", @@ -133,7 +133,7 @@ "directory": "unrar", "name": "unrar", "checksum": "8d5b3d5d6be2c14b74b02767430ade9c", - "url": "https://github.com/morpheus65535/bazarr-binaries/raw/master/bin/MacOSX/x86_64/unrar/unrar" + "url": "https://github.com/LavX/bazarr-binaries/raw/master/bin/MacOSX/x86_64/unrar/unrar" }, { "system": "Windows", @@ -141,7 +141,7 @@ "directory": "ffmpeg", "name": "ffmpeg.exe", "checksum": "43f89a5172c377e05ebb4c26a498a366", - "url": "https://github.com/morpheus65535/bazarr-binaries/raw/master/bin/Windows/i386/ffmpeg/ffmpeg.exe" + "url": "https://github.com/LavX/bazarr-binaries/raw/master/bin/Windows/i386/ffmpeg/ffmpeg.exe" }, { "system": "Windows", @@ -149,7 +149,7 @@ "directory": "ffmpeg", "name": "ffprobe.exe", "checksum": "a0d88aa85624070a8ab65ed99c214bea", - "url": "https://github.com/morpheus65535/bazarr-binaries/raw/master/bin/Windows/i386/ffmpeg/ffprobe.exe" + "url": "https://github.com/LavX/bazarr-binaries/raw/master/bin/Windows/i386/ffmpeg/ffprobe.exe" }, { "system": "Windows", @@ -157,6 +157,6 @@ "directory": "unrar", "name": "unrar.exe", "checksum": "4611a5b3f70a8d6c40776e0bfa3b3f36", - "url": "https://github.com/morpheus65535/bazarr-binaries/raw/master/bin/Windows/i386/unrar/UnRAR.exe" + "url": "https://github.com/LavX/bazarr-binaries/raw/master/bin/Windows/i386/unrar/UnRAR.exe" } ] diff --git a/bazarr/utilities/cache.py b/bazarr/utilities/cache.py index a43d536277..02e86fe191 100644 --- a/bazarr/utilities/cache.py +++ b/bazarr/utilities/cache.py @@ -11,9 +11,10 @@ from app.jobs_queue import jobs_queue -def cache_maintenance(job_id=None): +def cache_maintenance(job_id=None, wait_for_completion=False): if not job_id: - jobs_queue.add_job_from_function("Performing Cache Maintenance", is_progress=False) + jobs_queue.add_job_from_function("Performing Cache Maintenance", is_progress=False, + wait_for_completion=wait_for_completion) return main_cache_validity = 14 # days diff --git a/bazarr/utilities/filesystem.py b/bazarr/utilities/filesystem.py index fea29917a3..0fa919fa43 100644 --- a/bazarr/utilities/filesystem.py +++ b/bazarr/utilities/filesystem.py @@ -1,8 +1,27 @@ # coding=utf-8 import os +import logging import string +# System directories that should not be browsable (Linux/macOS) +# Note: /run is NOT blocked because /run/media is used for automounted +# removable media on many Linux distributions (GNOME, KDE, etc.) +_BLOCKED_PATHS = { + '/proc', '/sys', '/dev', '/snap', + '/boot', '/lost+found', '/swapfile', + '/etc', '/root', '/tmp', +} + + +def _is_path_blocked(path): + """Check if a path falls within a blocked system directory.""" + real = os.path.realpath(path) + for blocked in _BLOCKED_PATHS: + if real == blocked or real.startswith(blocked + os.sep): + return True + return False + def browse_bazarr_filesystem(path='#'): if path == '#' or path == '/' or path == '': @@ -16,11 +35,16 @@ def browse_bazarr_filesystem(path='#'): path = "/" dir_list = [f for f in os.listdir(path) if os.path.isdir(os.path.join(path, f))] else: + if _is_path_blocked(path): + logging.warning(f'Filesystem browse blocked for restricted path: {path}') + return {'directories': [], 'parent': os.path.dirname(path)} dir_list = [f for f in os.listdir(path) if os.path.isdir(os.path.join(path, f))] data = [] for item in dir_list: full_path = os.path.join(path, item, '') + if _is_path_blocked(full_path): + continue item = { "name": item, "path": full_path diff --git a/bazarr/utilities/health.py b/bazarr/utilities/health.py index 003414699f..019beda35a 100644 --- a/bazarr/utilities/health.py +++ b/bazarr/utilities/health.py @@ -14,9 +14,9 @@ from radarr.rootfolder import check_radarr_rootfolder -def check_health(job_id=None): +def check_health(job_id=None, wait_for_completion=False): if not job_id: - jobs_queue.add_job_from_function("Checking Health", is_progress=False) + jobs_queue.add_job_from_function("Checking Health", is_progress=False, wait_for_completion=wait_for_completion) return if settings.general.use_sonarr: diff --git a/bazarr/utilities/helper.py b/bazarr/utilities/helper.py index e8378bd847..cda9d05eff 100644 --- a/bazarr/utilities/helper.py +++ b/bazarr/utilities/helper.py @@ -1,6 +1,7 @@ # coding=utf-8 import os +import hmac import logging import hashlib @@ -10,13 +11,58 @@ from app.config import settings +PBKDF2_ITERATIONS = 600_000 + + +def hash_password(pw): + salt = os.urandom(16) + hashed = hashlib.pbkdf2_hmac('sha256', f"{pw}".encode('utf-8'), salt, PBKDF2_ITERATIONS) + return 'pbkdf2:' + salt.hex() + ':' + hashed.hex() + + +def _is_legacy_md5(stored_hash): + return not stored_hash.startswith('pbkdf2:') + + +def _verify_password(pw, stored_hash): + if stored_hash.startswith('pbkdf2:'): + try: + _, salt_hex, hash_hex = stored_hash.split(':', 2) + salt = bytes.fromhex(salt_hex) + expected = bytes.fromhex(hash_hex) + except (ValueError, TypeError): + logging.error('Corrupted PBKDF2 password hash in config. Re-set your password in settings.') + return False + actual = hashlib.pbkdf2_hmac('sha256', f"{pw}".encode('utf-8'), salt, PBKDF2_ITERATIONS) + return hmac.compare_digest(actual, expected) + else: + return hmac.compare_digest( + hashlib.md5(f"{pw}".encode('utf-8')).hexdigest(), + stored_hash + ) + + +def upgrade_password_hash(pw): + new_hash = hash_password(pw) + old_hash = settings.auth.password + settings.auth.password = new_hash + try: + from app.config import write_config + write_config() + logging.info('Upgraded password hash from MD5 to PBKDF2-SHA256') + except Exception: + settings.auth.password = old_hash + logging.exception('Failed to persist password hash upgrade, reverted to previous hash') + raise + + def check_credentials(user, pw, request, log_success=True): forwarded_for_ip_addr = request.environ.get('HTTP_X_FORWARDED_FOR') real_ip_addr = request.environ.get('HTTP_X_REAL_IP') ip_addr = forwarded_for_ip_addr or real_ip_addr or request.remote_addr username = settings.auth.username password = settings.auth.password - if hashlib.md5(f"{pw}".encode('utf-8')).hexdigest() == password and user == username: + if user == username and _verify_password(pw, password): if log_success: logging.info(f'Successful authentication from {ip_addr} for user {user}') return True @@ -25,6 +71,11 @@ def check_credentials(user, pw, request, log_success=True): return False +def needs_password_upgrade(): + password = settings.auth.password + return bool(password) and _is_legacy_md5(password) + + def get_subtitle_destination_folder(): fld_custom = str(settings.general.subfolder_custom).strip() if (settings.general.subfolder_custom and settings.general.subfolder != 'current') else None diff --git a/bazarr/utilities/plex_utils.py b/bazarr/utilities/plex_utils.py index 487ca28440..1ea147be89 100644 --- a/bazarr/utilities/plex_utils.py +++ b/bazarr/utilities/plex_utils.py @@ -2,7 +2,7 @@ import logging import requests -from app.config import settings +from app.config import settings, get_ssl_verify def get_plex_libraries_with_paths(): @@ -30,7 +30,7 @@ def get_plex_libraries_with_paths(): f"{server_url}/library/sections", headers={'X-Plex-Token': decrypted_token, 'Accept': 'application/json'}, timeout=5, - verify=False + verify=get_ssl_verify('plex') ) if response.status_code != 200: @@ -73,7 +73,7 @@ def _get_library_locations(server_url, section_key, token): f"{server_url}/library/sections/{section_key}", headers={'X-Plex-Token': token, 'Accept': 'application/json'}, timeout=5, - verify=False + verify=get_ssl_verify('plex') ) if response.status_code == 200: diff --git a/bazarr/utilities/post_processing.py b/bazarr/utilities/post_processing.py index d68ac9de73..094d8cb034 100644 --- a/bazarr/utilities/post_processing.py +++ b/bazarr/utilities/post_processing.py @@ -3,43 +3,51 @@ import os import re import sys +import shlex +import subprocess import logging from app.config import settings -# Wraps the input string within quotes & escapes the string def _escape(in_str): - raw_map = {8: r'\\b', 7: r'\\a', 12: r'\\f', 10: r'\\n', 13: r'\\r', 9: r'\\t', 11: r'\\v', 34: r'\"', 92: r'\\'} - raw_str = r''.join(raw_map.get(ord(i), i) for i in in_str) - return f"\"{raw_str}\"" + s = str(in_str) if in_str is not None else '' + if os.name == 'nt': + # cmd.exe: use subprocess.list2cmdline for proper Windows quoting + return subprocess.list2cmdline([s]) + return shlex.quote(s) + + +def _pp_sub(pattern, value, command): + """Substitute a placeholder, escaping backslashes for re.sub replacement.""" + escaped = _escape(value) + return re.sub(pattern, lambda _: escaped, command) def pp_replace(pp_command, episode, subtitles, language, language_code2, language_code3, episode_language, episode_language_code2, episode_language_code3, score, subtitle_id, provider, uploader, release_info, series_id, episode_id): - pp_command = re.sub(r'[\'"]?{{directory}}[\'"]?', _escape(os.path.dirname(episode)), pp_command) - pp_command = re.sub(r'[\'"]?{{episode}}[\'"]?', _escape(episode), pp_command) - pp_command = re.sub(r'[\'"]?{{episode_name}}[\'"]?', _escape(os.path.splitext(os.path.basename(episode))[0]), - pp_command) - pp_command = re.sub(r'[\'"]?{{subtitles}}[\'"]?', _escape(str(subtitles)), pp_command) - pp_command = re.sub(r'[\'"]?{{subtitles_language}}[\'"]?', _escape(str(language)), pp_command) - pp_command = re.sub(r'[\'"]?{{subtitles_language_code2}}[\'"]?', _escape(str(language_code2)), pp_command) - pp_command = re.sub(r'[\'"]?{{subtitles_language_code3}}[\'"]?', _escape(str(language_code3)), pp_command) - pp_command = re.sub(r'[\'"]?{{subtitles_language_code2_dot}}[\'"]?', - _escape(str(language_code2).replace(':', '.')), pp_command) - pp_command = re.sub(r'[\'"]?{{subtitles_language_code3_dot}}[\'"]?', - _escape(str(language_code3).replace(':', '.')), pp_command) - pp_command = re.sub(r'[\'"]?{{episode_language}}[\'"]?', _escape(str(episode_language)), pp_command) - pp_command = re.sub(r'[\'"]?{{episode_language_code2}}[\'"]?', _escape(str(episode_language_code2)), pp_command) - pp_command = re.sub(r'[\'"]?{{episode_language_code3}}[\'"]?', _escape(str(episode_language_code3)), pp_command) - pp_command = re.sub(r'[\'"]?{{score}}[\'"]?', _escape(str(score)), pp_command) - pp_command = re.sub(r'[\'"]?{{subtitle_id}}[\'"]?', _escape(str(subtitle_id)), pp_command) - pp_command = re.sub(r'[\'"]?{{provider}}[\'"]?', _escape(str(provider)), pp_command) - pp_command = re.sub(r'[\'"]?{{uploader}}[\'"]?', _escape(str(uploader)), pp_command) - pp_command = re.sub(r'[\'"]?{{release_info}}[\'"]?', _escape(str(release_info)), pp_command) - pp_command = re.sub(r'[\'"]?{{series_id}}[\'"]?', _escape(str(series_id)), pp_command) - pp_command = re.sub(r'[\'"]?{{episode_id}}[\'"]?', _escape(str(episode_id)), pp_command) + pp_command = _pp_sub(r'[\'"]?{{directory}}[\'"]?', os.path.dirname(episode), pp_command) + pp_command = _pp_sub(r'[\'"]?{{episode}}[\'"]?', episode, pp_command) + pp_command = _pp_sub(r'[\'"]?{{episode_name}}[\'"]?', os.path.splitext(os.path.basename(episode))[0], pp_command) + pp_command = _pp_sub(r'[\'"]?{{subtitles}}[\'"]?', str(subtitles), pp_command) + pp_command = _pp_sub(r'[\'"]?{{subtitles_language}}[\'"]?', str(language), pp_command) + pp_command = _pp_sub(r'[\'"]?{{subtitles_language_code2}}[\'"]?', str(language_code2), pp_command) + pp_command = _pp_sub(r'[\'"]?{{subtitles_language_code3}}[\'"]?', str(language_code3), pp_command) + pp_command = _pp_sub(r'[\'"]?{{subtitles_language_code2_dot}}[\'"]?', + str(language_code2).replace(':', '.'), pp_command) + pp_command = _pp_sub(r'[\'"]?{{subtitles_language_code3_dot}}[\'"]?', + str(language_code3).replace(':', '.'), pp_command) + pp_command = _pp_sub(r'[\'"]?{{episode_language}}[\'"]?', str(episode_language), pp_command) + pp_command = _pp_sub(r'[\'"]?{{episode_language_code2}}[\'"]?', str(episode_language_code2), pp_command) + pp_command = _pp_sub(r'[\'"]?{{episode_language_code3}}[\'"]?', str(episode_language_code3), pp_command) + pp_command = _pp_sub(r'[\'"]?{{score}}[\'"]?', str(score), pp_command) + pp_command = _pp_sub(r'[\'"]?{{subtitle_id}}[\'"]?', str(subtitle_id), pp_command) + pp_command = _pp_sub(r'[\'"]?{{provider}}[\'"]?', str(provider), pp_command) + pp_command = _pp_sub(r'[\'"]?{{uploader}}[\'"]?', str(uploader), pp_command) + pp_command = _pp_sub(r'[\'"]?{{release_info}}[\'"]?', str(release_info), pp_command) + pp_command = _pp_sub(r'[\'"]?{{series_id}}[\'"]?', str(series_id), pp_command) + pp_command = _pp_sub(r'[\'"]?{{episode_id}}[\'"]?', str(episode_id), pp_command) return pp_command diff --git a/custom_libs/subliminal/extensions.py b/custom_libs/subliminal/extensions.py index 61843ed410..f3b630b359 100644 --- a/custom_libs/subliminal/extensions.py +++ b/custom_libs/subliminal/extensions.py @@ -64,7 +64,7 @@ def register(self, entry_point): if ep.name in self.names(): raise ValueError('An extension with the same name already exist') - ext = self._load_one_plugin(ep, False, (), {}, False) + ext = self._load_one_plugin(ep, False, (), {}) self.extensions.append(ext) if self._extensions_by_name is not None: self._extensions_by_name[ext.name] = ext diff --git a/custom_libs/subliminal_patch/core.py b/custom_libs/subliminal_patch/core.py index 6ea2c1bbc7..3d57c2bf5a 100644 --- a/custom_libs/subliminal_patch/core.py +++ b/custom_libs/subliminal_patch/core.py @@ -23,8 +23,8 @@ from concurrent.futures import as_completed from .extensions import provider_registry -from .exceptions import MustGetBlacklisted -from .score import compute_score as default_compute_score +from .exceptions import APIThrottled, MustGetBlacklisted +from .score import compute_score, MAX_SCORES from subliminal.utils import hash_napiprojekt, hash_opensubtitles, hash_shooter, hash_thesubdb from subliminal.video import VIDEO_EXTENSIONS, Video, Episode, Movie from subliminal.core import guessit, ProviderPool, io, is_windows_special_path, \ @@ -238,6 +238,8 @@ def __init__(self, providers=None, provider_configs=None, blacklist=None, ban_li self._born = time.time() + self.provider_progress_callback = None + if not self.throttle_callback: self.throttle_callback = lambda x, y, ids=None, language=None: x @@ -376,6 +378,9 @@ def list_subtitles_provider(self, provider, video, languages): logger.info('Listing subtitles with provider %r and languages %r', provider, to_request) + if self.provider_progress_callback: + self.provider_progress_callback(provider) + try: results = self[provider].list_subtitles(video, to_request) seen = [] @@ -412,6 +417,15 @@ def list_subtitles_provider(self, provider, video, languages): return out + except APIThrottled as e: + ids = { + 'radarrId': video.radarrId if hasattr(video, 'radarrId') else None, + 'sonarrSeriesId': video.sonarrSeriesId if hasattr(video, 'sonarrSeriesId') else None, + 'sonarrEpisodeId': video.sonarrEpisodeId if hasattr(video, 'sonarrEpisodeId') else None, + } + logger.warning('Provider %r throttled: %s', provider, e) + self.throttle_callback(provider, e, ids=ids, language=list(languages)[0] if len(languages) else None) + except Exception as e: ids = { 'radarrId': video.radarrId if hasattr(video, 'radarrId') else None, @@ -598,7 +612,7 @@ def download_subtitle(self, subtitle): return True def download_best_subtitles(self, subtitles, video, languages, min_score=0, hearing_impaired=False, only_one=False, - compute_score=None, use_original_format=False): + use_original_format=False): """Download the best matching subtitles. patch: @@ -615,18 +629,15 @@ def download_best_subtitles(self, subtitles, video, languages, min_score=0, hear :param int min_score: minimum score for a subtitle to be downloaded. :param bool hearing_impaired: hearing impaired preference. :param bool only_one: download only one subtitle, not one per language. - :param compute_score: function that takes `subtitle` and `video` as positional arguments, - `hearing_impaired` as keyword argument and returns the score. :param bool use_original_format: preserve original subtitles format :return: downloaded subtitles. :rtype: list of :class:`~subliminal.subtitle.Subtitle` """ - compute_score = compute_score or default_compute_score use_hearing_impaired = hearing_impaired in ("prefer", "force HI") is_episode = isinstance(video, Episode) - max_score = sum(val for key, val in compute_score._scores['episode' if is_episode else 'movie'].items() if key != "hash") + max_score = MAX_SCORES['episode' if is_episode else 'movie'] # sort subtitles by score unsorted_subtitles = [] @@ -638,7 +649,9 @@ def download_best_subtitles(self, subtitles, video, languages, min_score=0, hear continue try: - matches = s.get_matches(video) + matches = s.matches if hasattr(s, 'matches') and isinstance(s.matches, set) and len(s.matches) \ + else s.get_matches(video) + except AttributeError: logger.error("%r: Match computation failed: %s", s, traceback.format_exc()) continue @@ -1159,7 +1172,7 @@ def download_subtitles(subtitles, pool_class=ProviderPool, **kwargs): pool.download_subtitle(subtitle) -def download_best_subtitles(videos, languages, min_score=0, hearing_impaired=False, only_one=False, compute_score=None, +def download_best_subtitles(videos, languages, min_score=0, hearing_impaired=False, only_one=False, pool_class=ProviderPool, throttle_time=0, **kwargs): r"""List and download the best matching subtitles. @@ -1172,8 +1185,6 @@ def download_best_subtitles(videos, languages, min_score=0, hearing_impaired=Fal :param int min_score: minimum score for a subtitle to be downloaded. :param bool hearing_impaired: hearing impaired preference. :param bool only_one: download only one subtitle, not one per language. - :param compute_score: function that takes `subtitle` and `video` as positional arguments, - `hearing_impaired` as keyword argument and returns the score. :param pool_class: class to use as provider pool. :type pool_class: :class:`ProviderPool`, :class:`AsyncProviderPool` or similar :param \*\*kwargs: additional parameters for the provided `pool_class` constructor. diff --git a/custom_libs/subliminal_patch/core_persistent.py b/custom_libs/subliminal_patch/core_persistent.py index 84568d58fa..888d65f420 100644 --- a/custom_libs/subliminal_patch/core_persistent.py +++ b/custom_libs/subliminal_patch/core_persistent.py @@ -49,8 +49,8 @@ def download_best_subtitles( min_score=0, hearing_impaired=False, only_one=False, - compute_score=None, use_original_format=False, + use_provider_priority=True, **kwargs ): downloaded_subtitles = defaultdict(list) @@ -70,14 +70,17 @@ def download_best_subtitles( # download best subtitles for video in checked_videos: logger.info("Downloading best subtitles for %r", video) + if use_provider_priority: + listed = pool_instance.list_subtitles_prioritized(video, languages - video.subtitle_languages, min_score=min_score) + else: + listed = pool_instance.list_subtitles(video, languages - video.subtitle_languages) subtitles = pool_instance.download_best_subtitles( - pool_instance.list_subtitles_prioritized(video, languages - video.subtitle_languages, min_score=min_score), + listed, video, languages, min_score=min_score, hearing_impaired=hearing_impaired, only_one=only_one, - compute_score=compute_score, use_original_format=use_original_format, ) logger.info("Downloaded %d subtitle(s)", len(subtitles)) diff --git a/custom_libs/subliminal_patch/exceptions.py b/custom_libs/subliminal_patch/exceptions.py index e5857beef5..0714f23080 100644 --- a/custom_libs/subliminal_patch/exceptions.py +++ b/custom_libs/subliminal_patch/exceptions.py @@ -10,7 +10,9 @@ class TooManyRequests(ProviderError): class APIThrottled(ProviderError): - pass + def __init__(self, *args, retry_after=None, **kwargs): + super().__init__(*args, **kwargs) + self.retry_after = retry_after class ForbiddenError(ProviderError): diff --git a/custom_libs/subliminal_patch/providers/legendasnet.py b/custom_libs/subliminal_patch/providers/legendasnet.py index 2bda948354..bc1a2ddf66 100644 --- a/custom_libs/subliminal_patch/providers/legendasnet.py +++ b/custom_libs/subliminal_patch/providers/legendasnet.py @@ -107,9 +107,19 @@ def login(self): "password": self.password }) - response = self.session.request("POST", self.server_url() + 'login', data=payload, headers=headersList) - if response.status_code != 200: - raise ConfigurationError('Failed to login and retrieve access token') + response = self.retry( + lambda: self.session.request("POST", self.server_url() + 'login', data=payload, headers=headersList, timeout=30), + amount=retry_amount, + retry_timeout=retry_timeout + ) + + if response.status_code == 429: + raise APIThrottled('Too many requests') + elif response.status_code in (401, 403): + raise ConfigurationError('Invalid username or password') + elif response.status_code != 200: + response.raise_for_status() + self.access_token = response.json().get('access_token') if not self.access_token: raise ConfigurationError('Access token not found in login response') @@ -164,7 +174,7 @@ def query(self, languages, video): raise ProviderError("Endpoint not found") elif res.status_code == 429: raise APIThrottled("Too many requests") - elif res.status_code == 403: + elif res.status_code in (401, 403): raise ConfigurationError("Invalid access token") elif res.status_code != 200: res.raise_for_status() @@ -241,7 +251,7 @@ def download_subtitle(self, subtitle): if r.status_code == 429: raise DownloadLimitExceeded("Daily download limit exceeded") - elif r.status_code == 403: + elif r.status_code in (401, 403): raise ConfigurationError("Invalid access token") elif r.status_code != 200: r.raise_for_status() diff --git a/custom_libs/subliminal_patch/providers/opensubtitles.py b/custom_libs/subliminal_patch/providers/opensubtitles.py index 7fa42577cf..01af0fcac8 100644 --- a/custom_libs/subliminal_patch/providers/opensubtitles.py +++ b/custom_libs/subliminal_patch/providers/opensubtitles.py @@ -114,11 +114,17 @@ def id(self): @property def series_name(self): - return self.series_re.match(self.movie_name).group('series_name') + match = self.series_re.match(self.movie_name) + if match: + return match.group('series_name') + return self.movie_name @property def series_title(self): - return self.series_re.match(self.movie_name).group('series_title') + match = self.series_re.match(self.movie_name) + if match: + return match.group('series_title') + return self.movie_name def get_matches(self, video): matches = set() @@ -132,7 +138,7 @@ def get_matches(self, video): if video.series and sanitize(self.series_name) == sanitize(video.series): matches.add('series') # year - if video.original_series and self.movie_year is None or video.year and video.year == self.movie_year: + if (video.original_series and self.movie_year is None) or (video.year and video.year == self.movie_year): matches.add('year') # season if video.season and self.series_season == video.season: @@ -224,7 +230,7 @@ def __init__(self, language, hearing_impaired, page_link, subtitle_id, matched_b def get_fps(self): try: return float(self.fps) - except: + except (ValueError, TypeError): return None def get_matches(self, video, hearing_impaired=False): @@ -250,7 +256,7 @@ def get_matches(self, video, hearing_impaired=False): sub_fps = None try: sub_fps = float(self.fps) - except ValueError: + except (ValueError, TypeError): pass # video has fps info, sub also, and sub's fps is greater than 0 @@ -397,8 +403,18 @@ def use_token_or_login(self, func): def initialize(self): if self.use_web_scraper: - # Skip authentication for scraper mode - logger.debug("Web scraper mode - skipping authentication") + # Verify scraper service is reachable before searching + try: + base_url = self.scraper_service_url.rstrip('/') + if not base_url.startswith(('http://', 'https://')): + base_url = f'http://{base_url}' + resp = requests.get(f'{base_url}/health', timeout=5) + resp.raise_for_status() + logger.info("Scraper service at %s is healthy", self.scraper_service_url) + except Exception as e: + raise ServiceUnavailable( + f'OpenSubtitles scraper at {self.scraper_service_url} is not reachable: {e}' + ) self.server = None self.token = None return @@ -610,7 +626,7 @@ def checked(fn, raise_api_limit=False): status_code = 506 else: status_code = int(response['status'][:3]) - except: + except Exception: status_code = None if status_code == 401: diff --git a/custom_libs/subliminal_patch/providers/opensubtitles_scraper.py b/custom_libs/subliminal_patch/providers/opensubtitles_scraper.py index 75677ed69a..dfe849e074 100644 --- a/custom_libs/subliminal_patch/providers/opensubtitles_scraper.py +++ b/custom_libs/subliminal_patch/providers/opensubtitles_scraper.py @@ -1,167 +1,224 @@ # coding=utf-8 """ -OpenSubtitles Web Scraper Implementation +OpenSubtitles Web Scraper Integration (v1 API) -This module provides HTTP-based communication with OpenSubtitles scraper services, -bypassing the need for traditional API authentication. +Uses the scraper service's REST API directly instead of the legacy +compatibility endpoint, passing all available metadata for best results. """ import base64 import logging import requests -import json +from subliminal.video import Episode from subzero.language import Language -from subliminal.exceptions import ServiceUnavailable, ConfigurationError +from subliminal.exceptions import ServiceUnavailable from subliminal_patch.exceptions import APIThrottled logger = logging.getLogger(__name__) +# Map opensubtitles 3-letter codes to 2-letter codes for the scraper +_LANG_3_TO_2 = { + 'eng': 'en', 'hun': 'hu', 'spa': 'es', 'fre': 'fr', 'ger': 'de', + 'ita': 'it', 'por': 'pt', 'rus': 'ru', 'chi': 'zh', 'jpn': 'ja', + 'kor': 'ko', 'ara': 'ar', 'dut': 'nl', 'pol': 'pl', 'tur': 'tr', + 'swe': 'sv', 'nor': 'no', 'dan': 'da', 'fin': 'fi', 'cze': 'cs', + 'rum': 'ro', 'hrv': 'hr', 'srp': 'sr', 'bul': 'bg', 'gre': 'el', + 'heb': 'he', 'tha': 'th', 'vie': 'vi', 'ind': 'id', 'may': 'ms', + 'per': 'fa', 'ukr': 'uk', 'est': 'et', 'lav': 'lv', 'lit': 'lt', + 'slv': 'sl', 'slo': 'sk', 'ice': 'is', 'cat': 'ca', 'bos': 'bs', + 'glg': 'gl', 'baq': 'eu', 'geo': 'ka', 'mac': 'mk', 'alb': 'sq', +} + class OpenSubtitlesScraperMixin: """ - Mixin class that provides web scraper functionality for OpenSubtitles provider. - This allows communication with scraper services that provide OpenSubtitles-compatible APIs. + Mixin class providing web scraper functionality via the v1 REST API. """ - - def _query_scraper(self, video, languages, hash=None, size=None, imdb_id=None, query=None, season=None, - episode=None, tag=None, use_tag_search=False, only_foreign=False, also_foreign=False): - """ - Query the scraper service for subtitles. - - This method converts the traditional OpenSubtitles XML-RPC format to HTTP requests - that are compatible with scraper services. - """ - logger.info('Querying scraper service at %s', self.scraper_service_url) - - # Build search criteria similar to the API version - criteria = [] - if hash and size: - criteria.append({'moviehash': hash, 'moviebytesize': str(size)}) - if use_tag_search and tag: - criteria.append({'tag': tag}) - if imdb_id: - if season and episode: - criteria.append({'imdbid': imdb_id[2:], 'season': season, 'episode': episode}) - else: - criteria.append({'imdbid': imdb_id[2:]}) - - if not criteria: - raise ValueError('Not enough information') - # Add language information - for criterion in criteria: - criterion['sublanguageid'] = ','.join(sorted(l.opensubtitles for l in languages)) + def _get_scraper_base_url(self): + base_url = self.scraper_service_url.rstrip('/') + if not base_url.startswith(('http://', 'https://')): + base_url = f'http://{base_url}' + return base_url + + def _scraper_request(self, endpoint, data): + """Make a POST request to the scraper service.""" + url = f"{self._get_scraper_base_url()}{endpoint}" + headers = { + 'Content-Type': 'application/json', + 'User-Agent': 'Bazarr-OpenSubtitles-Scraper/2.0' + } + + logger.debug('Scraper request: %s %s', url, data) + + response = requests.post(url, json=data, headers=headers, timeout=120) + + if response.status_code in (429, 503): + retry_after_str = response.headers.get("Retry-After") + response.close() + retry_after = int(retry_after_str) if retry_after_str and retry_after_str.isdigit() else None + msg = "Scraper service busy" + if retry_after: + msg = f"{msg}, retry after {retry_after}s" + raise APIThrottled(msg, retry_after=retry_after) + + response.raise_for_status() + result = response.json() + response.close() + return result + + def _query_scraper(self, video, languages, hash=None, size=None, imdb_id=None, query=None, + season=None, episode=None, tag=None, use_tag_search=False, + only_foreign=False, also_foreign=False): + """Query the scraper v1 API with full metadata.""" + logger.info('Querying scraper v1 API at %s', self.scraper_service_url) + + is_episode = isinstance(video, Episode) + + # Build the best possible query string from available info + search_query = '' + if query and isinstance(query, list) and query[0]: + search_query = query[0] + elif query and isinstance(query, str): + search_query = query + elif hasattr(video, 'series') and video.series: + search_query = video.series + elif hasattr(video, 'title') and video.title: + search_query = video.title + + # Get year from video + year = getattr(video, 'year', None) try: - # Make HTTP request to scraper service - response = self._make_scraper_request('/search', { - 'criteria': criteria, - 'only_foreign': only_foreign, - 'also_foreign': also_foreign + # Step 1: Search for the movie/show + search_endpoint = '/api/v1/search/tv' if is_episode else '/api/v1/search/movies' + search_data = { + 'query': search_query, + 'imdb_id': imdb_id if imdb_id else None, + 'year': year, + 'kind': 'episode' if is_episode else 'movie', + } + + logger.info('Scraper search: %s query=%r imdb=%s year=%s', + search_endpoint, search_query, imdb_id, year) + + search_response = self._scraper_request(search_endpoint, search_data) + + results = search_response.get('results', []) + if not results: + logger.info('Scraper: no results from %s for %r', search_endpoint, search_query) + return [] + + # Step 2: Select best matching result + best_result = self._select_best_result(results, imdb_id, search_query, year) + if not best_result: + logger.info('Scraper: no matching result for imdb=%s query=%r', imdb_id, search_query) + return [] + + movie_url = best_result.get('url') + if not movie_url: + logger.warning('Scraper: search result has no URL') + return [] + + logger.info('Scraper: selected result: %s (%s) - %s', + best_result.get('title'), best_result.get('year'), movie_url) + + # Step 3: Get subtitle listings + lang_codes = [] + for lang in languages: + code_3 = lang.opensubtitles + code_2 = _LANG_3_TO_2.get(code_3, code_3) + lang_codes.append(code_2) + + subs_response = self._scraper_request('/api/v1/subtitles', { + 'movie_url': movie_url, + 'languages': lang_codes, }) - - if not response or not response.get('data'): - logger.info('No subtitles found from scraper service') + + subtitle_list = subs_response.get('subtitles', []) + if not subtitle_list: + logger.info('Scraper: no subtitles found at %s', movie_url) return [] - - return self._parse_scraper_response(response['data'], languages, only_foreign, also_foreign, video) - + + logger.info('Scraper: found %d subtitles, parsing...', len(subtitle_list)) + + # Step 4: Convert to bazarr subtitle objects + return self._parse_v1_subtitles( + subtitle_list, best_result, languages, season, episode, + only_foreign, also_foreign, video, imdb_id, + hash=hash, search_query=search_query + ) + except APIThrottled: raise except requests.RequestException as e: logger.error('Scraper service request failed: %s', e) raise ServiceUnavailable(f'Scraper service unavailable: {e}') except Exception as e: - logger.error('Unexpected error querying scraper service: %s', e) + logger.error('Unexpected error querying scraper: %s', e) raise ServiceUnavailable(f'Scraper service error: {e}') - def _make_scraper_request(self, endpoint, data): - """ - Make an HTTP request to the scraper service. - - Args: - endpoint: API endpoint (e.g., '/search', '/download') - data: Request payload - - Returns: - dict: JSON response from scraper service - """ - # Ensure URL has proper protocol scheme - base_url = self.scraper_service_url.rstrip('/') - if not base_url.startswith(('http://', 'https://')): - base_url = f'http://{base_url}' - - url = f"{base_url}{endpoint}" - - headers = { - 'Content-Type': 'application/json', - 'User-Agent': 'Bazarr-OpenSubtitles-Scraper/1.0' - } - - logger.debug('Making scraper request to %s with data: %s', url, data) - - response = requests.post( - url, - json=data, - headers=headers, - timeout=120 # Increased timeout for scraper service (IMDB lookups + page navigation) - ) + def _select_best_result(self, results, imdb_id, query, year): + """Select the best search result by matching IMDB ID, title, and year.""" + if not results: + return None - if response.status_code in (429, 503): - retry_after = response.headers.get("Retry-After") - response.close() - message = "Scraper service busy" - if retry_after: - message = f"{message}, retry after {retry_after}s" - raise APIThrottled(message) + # Exact IMDB match is best + if imdb_id: + for r in results: + if r.get('imdb_id') == imdb_id: + return r - response.raise_for_status() - return response.json() - - def _parse_scraper_response(self, data, languages, only_foreign, also_foreign, video): - """ - Parse the scraper service response and create subtitle objects. - - Args: - data: Response data from scraper service - languages: Requested languages - only_foreign: Only foreign/forced subtitles - also_foreign: Include foreign/forced subtitles - video: Video object for matching - - Returns: - list: List of OpenSubtitlesSubtitle objects - """ + # Score remaining results + scored = [] + query_lower = (query or '').lower().strip() + for r in results: + score = 0 + title_lower = (r.get('title') or '').lower().strip() + + # Title matching + if query_lower and title_lower: + if title_lower == query_lower: + score += 10 + elif query_lower in title_lower or title_lower in query_lower: + score += 5 + + # Year matching + if year and r.get('year') == year: + score += 3 + + # Prefer results with more subtitles + score += min(r.get('subtitle_count', 0) / 100, 2) + + scored.append((score, r)) + + scored.sort(key=lambda x: x[0], reverse=True) + return scored[0][1] if scored else results[0] + + def _parse_v1_subtitles(self, subtitle_list, search_result, languages, season, episode, + only_foreign, also_foreign, video, imdb_id, + hash=None, search_query=None): + """Convert v1 API subtitle objects to bazarr OpenSubtitlesSubtitle objects.""" subtitles = [] - - for subtitle_item in data: + series_title = search_result.get('title', '') + result_year = search_result.get('year') + result_imdb = search_result.get('imdb_id', '') + + for sub in subtitle_list: try: - # Parse subtitle item (similar to API version) - language = Language.fromopensubtitles(subtitle_item['SubLanguageID']) - hearing_impaired = bool(int(subtitle_item.get('SubHearingImpaired', 0))) - page_link = subtitle_item.get('SubtitlesLink', '') - subtitle_id = int(subtitle_item['IDSubtitleFile']) - matched_by = subtitle_item.get('MatchedBy', 'hash') - movie_kind = subtitle_item.get('MovieKind', 'movie') - hash = subtitle_item.get('MovieHash', '') - movie_name = subtitle_item.get('MovieName', '') - movie_release_name = subtitle_item.get('MovieReleaseName', '') - movie_year = int(subtitle_item['MovieYear']) if subtitle_item.get('MovieYear') else None - - # Handle IMDB ID - if subtitle_item.get('SeriesIMDBParent'): - movie_imdb_id = 'tt' + subtitle_item['SeriesIMDBParent'] - elif subtitle_item.get('IDMovieImdb'): - movie_imdb_id = 'tt' + subtitle_item['IDMovieImdb'] - else: - movie_imdb_id = None - - movie_fps = subtitle_item.get('MovieFPS') - series_season = int(subtitle_item['SeriesSeason']) if subtitle_item.get('SeriesSeason') else None - series_episode = int(subtitle_item['SeriesEpisode']) if subtitle_item.get('SeriesEpisode') else None - filename = subtitle_item.get('SubFileName', '') - encoding = subtitle_item.get('SubEncoding') - foreign_parts_only = bool(int(subtitle_item.get('SubForeignPartsOnly', 0))) + # Map 2-letter language back to opensubtitles format + lang_2 = sub.get('language', '') + try: + language = Language.fromietf(lang_2) + except Exception: + try: + language = Language(lang_2) + except Exception: + logger.debug('Skipping subtitle with unknown language: %s', lang_2) + continue + + hearing_impaired = sub.get('hearing_impaired', False) + foreign_parts_only = sub.get('forced', False) # Apply foreign/forced filtering if only_foreign and not foreign_parts_only: @@ -171,76 +228,89 @@ def _parse_scraper_response(self, data, languages, only_foreign, also_foreign, v elif (also_foreign or only_foreign) and foreign_parts_only: language = Language.rebuild(language, forced=True) - # Set hearing impaired language if hearing_impaired: language = Language.rebuild(language, hi=True) if language not in languages: continue + subtitle_id = int(sub['subtitle_id']) + filename = sub.get('filename', '') + release_name = sub.get('release_name', '') + fps = sub.get('fps') + download_url = sub.get('download_url', '') + + # Build movie_name in the format bazarr expects + is_episode = season is not None and episode is not None + if is_episode and series_title: + movie_name = f'"{series_title}" {release_name}' + movie_kind = 'episode' + else: + movie_name = series_title or release_name + movie_kind = 'movie' + + movie_imdb_id = result_imdb if result_imdb else (imdb_id or None) + if movie_imdb_id and not movie_imdb_id.startswith('tt'): + movie_imdb_id = f'tt{movie_imdb_id}' + # IMDB ID matching - if video.imdb_id and movie_imdb_id and (movie_imdb_id != video.imdb_id): + if video.imdb_id and movie_imdb_id and movie_imdb_id != video.imdb_id: continue - query_parameters = subtitle_item.get("QueryParameters", {}) + query_parameters = { + 'query': search_query, + 'imdb_id': imdb_id, + 'season': season, + 'episode': episode, + } - # Create subtitle object subtitle = self.subtitle_class( - language, hearing_impaired, page_link, subtitle_id, matched_by, - movie_kind, hash, movie_name, movie_release_name, movie_year, movie_imdb_id, - series_season, series_episode, query_parameters, filename, encoding, - movie_fps, skip_wrong_fps=self.skip_wrong_fps + language, hearing_impaired, download_url, subtitle_id, + 'imdbid' if imdb_id else 'query', + movie_kind, hash or '', movie_name, release_name, result_year, movie_imdb_id, + season if is_episode else None, + episode if is_episode else None, + query_parameters, filename, None, + fps, skip_wrong_fps=self.skip_wrong_fps ) - - # Set additional attributes needed for matching - subtitle.uploader = subtitle_item.get('UserNickName', 'anonymous') - - # For TV series, set movie_name which is used as series_name in matching - # The series_name property uses movie_name internally - if movie_kind == 'episode': - if not movie_name and movie_release_name: - # Extract series name from release name if movie_name is empty - # e.g., "The Exchange" Bank of Tomorrow -> "The Exchange" - import re - series_match = re.match(r'^"([^"]+)"', movie_release_name) - if series_match: - subtitle.movie_name = series_match.group(1) - else: - # Fallback: use first part of release name - subtitle.movie_name = movie_release_name.split()[0] if movie_release_name else 'Unknown' - # movie_name is already set correctly from the API response - - logger.debug('Found subtitle %r by %s via scraper', subtitle, matched_by) + + subtitle.uploader = sub.get('uploader', 'anonymous') + # Store download_url for the download step + subtitle.scraper_download_url = download_url + + logger.debug('Found scraper subtitle: %s [%s] by %s (%d downloads)', + filename, lang_2, subtitle.uploader, sub.get('download_count', 0)) subtitles.append(subtitle) - + except (KeyError, ValueError, TypeError) as e: - logger.warning('Failed to parse subtitle item from scraper: %s', e) + logger.warning('Failed to parse scraper subtitle: %s', e) continue + logger.info('Scraper: returning %d subtitles after filtering', len(subtitles)) return subtitles def _download_subtitle_scraper(self, subtitle): - """ - Download subtitle content from scraper service. - - Args: - subtitle: OpenSubtitlesSubtitle object - """ - logger.info('Downloading subtitle %r via scraper', subtitle) - + """Download subtitle content via the v1 API.""" + logger.info('Downloading subtitle %d via scraper', subtitle.subtitle_id) + + download_url = getattr(subtitle, 'scraper_download_url', None) + if not download_url: + download_url = f'https://www.opensubtitles.org/en/subtitles/{subtitle.subtitle_id}' + try: - response = self._make_scraper_request('/download', { - 'subtitle_id': str(subtitle.subtitle_id) + response = self._scraper_request('/api/v1/download/subtitle', { + 'subtitle_id': str(subtitle.subtitle_id), + 'download_url': download_url, }) - - if response and response.get('data'): - # The scraper returns base64-encoded subtitle content - subtitle.content = base64.b64decode(response['data']) + + content = response.get('content') + if content: + subtitle.content = base64.b64decode(content) else: raise ServiceUnavailable('No subtitle content received from scraper') - + except APIThrottled: raise - except requests.RequestException as e: + except Exception as e: logger.error('Failed to download subtitle from scraper: %s', e) raise ServiceUnavailable(f'Scraper download failed: {e}') diff --git a/custom_libs/subliminal_patch/providers/opensubtitlescom.py b/custom_libs/subliminal_patch/providers/opensubtitlescom.py index ccf5cbddbd..f07ecfb68b 100644 --- a/custom_libs/subliminal_patch/providers/opensubtitlescom.py +++ b/custom_libs/subliminal_patch/providers/opensubtitlescom.py @@ -348,9 +348,10 @@ def query(self, languages, video): params.append(('episode_number', self.video.episode)) if self.video.season: params.append(('season_number', self.video.season)) + if self.video.series_imdb_id: params.append(('parent_imdb_id', self.sanitize_external_ids(self.video.series_imdb_id))) - if title_id: + elif title_id: params.append(('parent_feature_id', title_id)) else: if not imdb_id and not title_id: diff --git a/custom_libs/subliminal_patch/providers/subssabbz.py b/custom_libs/subliminal_patch/providers/subssabbz.py index 63120fc116..5485d75396 100644 --- a/custom_libs/subliminal_patch/providers/subssabbz.py +++ b/custom_libs/subliminal_patch/providers/subssabbz.py @@ -6,12 +6,13 @@ import os import codecs import unicodedata +import time from hashlib import sha1 from random import randint from bs4 import BeautifulSoup from zipfile import ZipFile, is_zipfile from rarfile import RarFile, is_rarfile -from requests import Session +from requests import Session, HTTPError from guessit import guessit from dogpile.cache.api import NO_VALUE from subliminal_patch.providers import Provider @@ -51,6 +52,27 @@ def fix_movie_naming(title): }, True) +def _retry_on_403(method): + def retry(self, *args, **kwargs): + retries = 0 + while 3 > retries: + try: + return method(self, *args, **kwargs) + except HTTPError as error: + if error.response.status_code != 403: + raise + + retries += 1 + + logger.debug("403 returned. Retrying in 10 seconds") + time.sleep(10) + + logger.debug("Retries limit exceeded. Ignoring query") + return [] + + return retry + + class SubsSabBzSubtitle(Subtitle): """SubsSabBz Subtitle.""" provider_name = 'subssabbz' @@ -140,6 +162,7 @@ def initialize(self): def terminate(self): self.session.close() + @_retry_on_403 def query(self, language, video): subtitles = [] isEpisode = isinstance(video, Episode) diff --git a/custom_libs/subliminal_patch/providers/subx.py b/custom_libs/subliminal_patch/providers/subx.py index 2ed3b2308a..a46a71e4be 100644 --- a/custom_libs/subliminal_patch/providers/subx.py +++ b/custom_libs/subliminal_patch/providers/subx.py @@ -256,9 +256,9 @@ def run_query(self, query, video, video_type, season=None, episode=None): return [] elif response.status_code == 429: - # Rate limited - exponential backoff + # Rate limited - use Retry-After header if available if attempt < max_retries - 1: - wait_time = 2 ** attempt + wait_time = int(response.headers.get("Retry-After", 60 * (attempt + 1))) logger.warning("Rate limit hit, waiting %ds before retry %d/%d", wait_time, attempt + 1, max_retries) time.sleep(wait_time) @@ -282,6 +282,33 @@ def run_query(self, query, video, video_type, season=None, episode=None): # Success response.raise_for_status() data = response.json() + + # Proactively slow down if approaching rate limit + remaining = response.headers.get("X-RateLimit-Remaining") + limit = response.headers.get("X-RateLimit-Limit") + reset = response.headers.get("X-RateLimit-Reset") + + if remaining is not None and limit is not None: + try: + remaining_int = int(remaining) + limit_int = int(limit) + + # Slow down when below 20% of quota + if remaining_int < limit_int * 0.2: + if reset is not None: + # Wait exactly until the window resets + wait_time = max(0, int(reset) - int(time.time())) + else: + wait_time = 2 # Fallback + + logger.warning( + "Approaching SubX rate limit (%d/%d remaining), waiting %ds", + remaining_int, limit_int, wait_time + ) + time.sleep(wait_time) + except ValueError: + pass + break # Exit retry loop except Exception as e: @@ -343,8 +370,8 @@ def run_query(self, query, video, video_type, season=None, episode=None): if not page_url and item.get("id"): page_url = f"{_SUBX_BASE_URL}/api/subtitles/{item['id']}" - # Detect language variant (Spain vs LATAM) from description - description = item.get("description", "") + # Detect language variant (Spain vs LatAm) from description + description = item.get("description") or "" spain = _SPANISH_RE.search(description.lower()) is not None language = Language.fromalpha2("es") if spain else Language("spa", "MX") @@ -369,7 +396,7 @@ def run_query(self, query, video, video_type, season=None, episode=None): page_url = f"{_SUBX_BASE_URL}/api/subtitles/{item['id']}" # Detect language variant from description - description = item.get("description", "") + description = item.get("description") or "" spain = _SPANISH_RE.search(description.lower()) is not None language = Language.fromalpha2("es") if spain else Language("spa", "MX") diff --git a/custom_libs/subliminal_patch/score.py b/custom_libs/subliminal_patch/score.py index 2bcbd9776d..b0810b34fd 100644 --- a/custom_libs/subliminal_patch/score.py +++ b/custom_libs/subliminal_patch/score.py @@ -31,33 +31,38 @@ def framerate_equal(source, check): DEFAULT_SCORES = { "episode": { "hash": 359, - "series": 180, + "series": 160, "year": 90, "season": 30, "episode": 30, - "release_group": 14, - "source": 7, - "audio_codec": 3, - "resolution": 2, - "video_codec": 2, - "streaming_service": 1, + "source": 25, + "release_group": 20, + "audio_codec": 1, + "resolution": 1, + "video_codec": 1, "hearing_impaired": 1, + "streaming_service": 1, }, "movie": { - "hash": 119, + "hash": 179, "title": 60, - "year": 30, - "release_group": 13, - "source": 7, - "audio_codec": 3, - "resolution": 2, - "video_codec": 2, - "streaming_service": 1, - "edition": 1, + "year": 40, + "source": 30, + "edition": 30, + "release_group": 15, + "audio_codec": 1, + "resolution": 1, + "video_codec": 1, "hearing_impaired": 1, + "streaming_service": 1, }, } +MAX_SCORES = { + "episode": sum(v for k, v in DEFAULT_SCORES["episode"].items() if k != "hash"), + "movie": sum(v for k, v in DEFAULT_SCORES["movie"].items() if k != "hash"), +} + def _check_hash_sum(scores: dict): hash_val = scores["hash"] diff --git a/custom_libs/subzero/analytics.py b/custom_libs/subzero/analytics.py deleted file mode 100644 index bda9d62521..0000000000 --- a/custom_libs/subzero/analytics.py +++ /dev/null @@ -1,34 +0,0 @@ -# coding=utf-8 - -from __future__ import absolute_import -import struct -import binascii - -from pyga.requests import Event, Page, Tracker, Session, Visitor, Config - - -def track_event(category=None, action=None, label=None, value=None, identifier=None, first_use=None, add=None, - noninteraction=True): - anonymousConfig = Config() - anonymousConfig.anonimize_ip_address = True - - tracker = Tracker('UA-86466078-1', 'none', conf=anonymousConfig) - visitor = Visitor() - - # convert the last 8 bytes of the machine identifier to an integer to get a "unique" user - visitor.unique_id = struct.unpack("!I", binascii.unhexlify(identifier[32:]))[0]/2 - - if add: - # add visitor's ip address (will be anonymized) - visitor.ip_address = add - - if first_use: - visitor.first_visit_time = first_use - - session = Session() - event = Event(category=category, action=action, label=label, value=value, noninteraction=noninteraction) - path = u"/" + u"/".join([category, action, label]) - page = Page(path.lower()) - - tracker.track_event(event, session, visitor) - tracker.track_pageview(page, session, visitor) diff --git a/custom_libs/subzero/modification/__init__.py b/custom_libs/subzero/modification/__init__.py index 73d75dea9a..4786d344ed 100644 --- a/custom_libs/subzero/modification/__init__.py +++ b/custom_libs/subzero/modification/__init__.py @@ -2,5 +2,5 @@ from __future__ import absolute_import from .registry import registry -from .mods import hearing_impaired, ocr_fixes, fps, offset, common, color, emoji +from .mods import hearing_impaired, ocr_fixes, fps, offset, common, color, emoji, two_point_fit from .main import SubtitleModifications, SubMod diff --git a/custom_libs/subzero/modification/mods/two_point_fit.py b/custom_libs/subzero/modification/mods/two_point_fit.py new file mode 100644 index 0000000000..8c8e732f8a --- /dev/null +++ b/custom_libs/subzero/modification/mods/two_point_fit.py @@ -0,0 +1,29 @@ +# coding=utf-8 + +from __future__ import absolute_import +import logging + +from subzero.modification.mods import SubtitleModification +from subzero.modification import registry + +logger = logging.getLogger(__name__) + + +class TwoPointFit(SubtitleModification): + identifier = "two_point_fit" + description = "Use first and last sentences to linearly align timing of the subtitles" + exclusive = False + advanced = True + modifies_whole_file = True + + def modify(self, content, debug=False, parent=None, **kwargs): + + """ + Place first sentence at 00:00:00 and scale until duration matches, then offset back + """ + + parent.f.shift(h=-int(kwargs.get("rh", 0)), m=-int(kwargs.get("rm", 0)), s=-int(kwargs.get("rs", 0)), ms=-int(kwargs.get("rms", 0))) + parent.f.transform_framerate(float(kwargs.get("from")), float(kwargs.get("to"))) + parent.f.shift(h=int(kwargs.get("oh", 0)), m=int(kwargs.get("om", 0)), s=int(kwargs.get("os", 0)), ms=int(kwargs.get("oms", 0))) + +registry.register(TwoPointFit) diff --git a/docker-compose.yml b/docker-compose.yml index 4aced79eff..e5fa84c580 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,7 +1,7 @@ # ============================================================================= -# Bazarr LavX Fork - Docker Compose +# Bazarr+ Docker Compose # ============================================================================= -# Production deployment configuration with OpenSubtitles.org Scraper +# Production deployment configuration # # Usage: # docker compose up -d diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index 29df93ea55..170fa7fa01 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -14,14 +14,19 @@ set -e PUID=${PUID:-1000} PGID=${PGID:-1000} -echo " -โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•— -โ•‘ Bazarr (LavX Fork) Docker Container โ•‘ -โ•‘ โ•‘ -โ•‘ OpenSubtitles.org scraper support included โ•‘ -โ•‘ https://github.com/LavX/bazarr โ•‘ -โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ• -" +echo ' +__/\\\\\\\\\\\\\_________________________________________________________________________________________ + _\/\\\/////////\\\_______________________________________________________________________________________ + _\/\\\_______\/\\\______________________________________________________________________________/\\\_____ + _\/\\\\\\\\\\\\\\___/\\\\\\\\\_____/\\\\\\\\\\\__/\\\\\\\\\_____/\\/\\\\\\\___/\\/\\\\\\\______\/\\\_____ + _\/\\\/////////\\\_\////////\\\___\///////\\\/__\////////\\\___\/\\\/////\\\_\/\\\/////\\\__/\\\\\\\\\\\_ + _\/\\\_______\/\\\___/\\\\\\\\\\_______/\\\/______/\\\\\\\\\\__\/\\\___\///__\/\\\___\///__\/////\\\///__ + _\/\\\_______\/\\\__/\\\/////\\\_____/\\\/_______/\\\/////\\\__\/\\\_________\/\\\_____________\/\\\_____ + _\/\\\\\\\\\\\\\/__\//\\\\\\\\/\\__/\\\\\\\\\\\_\//\\\\\\\\/\\_\/\\\_________\/\\\_____________\///_____ + _\/////////////_____\////////\//__\///////////___\////////\//__\///__________\///________________________ + +Repository: https://github.com/LavX/bazarr +' echo "Starting with UID: $PUID, GID: $PGID" diff --git a/docs/AI_TRANSLATOR_INVESTIGATION.md b/docs/AI_TRANSLATOR_INVESTIGATION.md deleted file mode 100644 index ab2ede82aa..0000000000 --- a/docs/AI_TRANSLATOR_INVESTIGATION.md +++ /dev/null @@ -1,248 +0,0 @@ -# AI Subtitle Translator Integration - Investigation Report - -## Executive Summary - -This document details the findings from investigating two issues with the AI Subtitle Translator integration in Bazarr: -1. Configuration not being applied to the microservice -2. Understanding how AI translation gets triggered - ---- - -## Issue 1: Configuration Not Being Applied to Microservice - -### Symptoms -- User enters API key and Max Concurrent Jobs in Settings -- Service Status panel shows "API Key: ร— Not Set" and "Max Concurrent: 2" (default) - -### Root Cause Analysis - -The architecture involves **two separate systems**: - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ BAZARR โ”‚ โ”‚ AI SUBTITLE TRANSLATOR โ”‚ -โ”‚ โ”‚ โ”‚ (Microservice) โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ Frontend Settings Page โ”‚ โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ - API Key input โ”‚ โ”‚ โ”‚ โ”‚ /api/v1/status โ”‚ โ”‚ -โ”‚ โ”‚ - Max Concurrent selector โ”‚โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”‚ โ”‚ Returns microservice's โ”‚ โ”‚ -โ”‚ โ”‚ - Model selector โ”‚ โ”‚ โ”‚ โ”‚ internal config (NOT โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ Bazarr's config) โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ”‚ โ–ผ โ”‚ โ”‚ โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ Bazarr Config (config.yaml)โ”‚ โ”‚ โ”‚ โ”‚ /api/v1/jobs/translate โ”‚ โ”‚ -โ”‚ โ”‚ - openrouter_api_key โ”‚ โ”‚ โ”‚ โ”‚ Receives config PER- โ”‚ โ”‚ -โ”‚ โ”‚ - openrouter_max_concurrentโ”‚ โ”‚ โ”‚ โ”‚ REQUEST in payload โ”‚ โ”‚ -โ”‚ โ”‚ - openrouter_model โ”‚ โ”‚ โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ โ”‚ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - -#### Data Flow When Saving Settings: -1. User enters values in frontend Settings page -2. Frontend POSTs to `/api/system/settings` -3. Bazarr saves to its config file (`config.yaml`) -4. **Config is NOT sent to microservice** - settings are saved locally only - -#### Data Flow When Translating: -1. Translation request is submitted -2. [`OpenRouterTranslatorService._submit_and_poll()`](bazarr/subtitles/tools/translate/services/openrouter_translator.py:110) creates payload WITH config: -```python -payload = { - ... - "config": { - "apiKey": settings.translator.openrouter_api_key, - "model": settings.translator.openrouter_model, - "temperature": settings.translator.openrouter_temperature, - } -} -``` -3. **Problem**: `maxConcurrent` is NOT included in the payload! - -#### Data Flow When Viewing Status Panel: -1. [`TranslatorStatusPanel`](frontend/src/components/TranslatorStatus.tsx:146) calls `useTranslatorStatus()` -2. Fetches from Bazarr API `/translator/status` -3. [`TranslatorStatus.get()`](bazarr/api/translator/translator.py:29) proxies to microservice `/api/v1/status` -4. **Returns microservice's internal config** (defaults), not Bazarr's saved config - -### The Design Issue - -The current design sends config **per-request** (at translation time), but: -1. **`max_concurrent` is missing** from the translation payload -2. **Status panel shows wrong data** - displays microservice defaults, not Bazarr's settings - -### Solution - -#### Fix 1: Add `maxConcurrent` to translation payload - -In [`bazarr/subtitles/tools/translate/services/openrouter_translator.py`](bazarr/subtitles/tools/translate/services/openrouter_translator.py:137), add `maxConcurrent`: - -```python -# Line 136-142 -payload = { - ... - "config": { - "apiKey": settings.translator.openrouter_api_key, - "model": settings.translator.openrouter_model, - "temperature": settings.translator.openrouter_temperature, - "maxConcurrent": settings.translator.openrouter_max_concurrent, # ADD THIS - } -} -``` - -#### Fix 2: Update Status Panel to show Bazarr's config - -Option A: **Modify Status Panel to show Bazarr settings** (Recommended) -- Update frontend to fetch Bazarr's settings from `/api/system/settings` -- Display Bazarr's config alongside microservice status - -Option B: **Add endpoint to sync config to microservice** -- Add a POST method to [`TranslatorConfig`](bazarr/api/translator/translator.py:128) -- Call it when settings are saved -- Microservice would need a `/api/v1/config` POST endpoint - ---- - -## Issue 2: How AI Translation Gets Triggered - -### Current Behavior: Manual Only - -AI translation is **NOT automatic**. It must be manually triggered by the user. - -### Translation Trigger Flow - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ MANUAL TRANSLATION FLOW โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - -User Action in Bazarr UI: -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Episode/Movie Detail Page โ”‚ -โ”‚ Click on subtitle file โ”‚ -โ”‚ Select "Translate" action โ”‚ -โ”‚ Choose target language โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ–ผ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Frontend API Call โ”‚ -โ”‚ PATCH /api/subtitles/subtitles โ”‚ -โ”‚ action: "translate" โ”‚ -โ”‚ language: "target_lang" โ”‚ -โ”‚ path: "/path/to/subtitle.srt" โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ–ผ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ bazarr/api/subtitles/ โ”‚ -โ”‚ subtitles.py:170-181 โ”‚ -โ”‚ if action == 'translate': โ”‚ -โ”‚ translate_subtitles_file() โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ–ผ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ bazarr/subtitles/tools/ โ”‚ -โ”‚ translate/main.py:12 โ”‚ -โ”‚ translate_subtitles_file() โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ–ผ -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ TranslatorFactory โ”‚ -โ”‚ .create_translator() โ”‚ -โ”‚ Based on translator_type: โ”‚ -โ”‚ - google_translate โ”‚ -โ”‚ - gemini โ”‚ -โ”‚ - lingarr โ”‚ -โ”‚ - openrouter (AI Translator) โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ - โ”‚ - โ–ผ (if openrouter) -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ OpenRouterTranslatorService โ”‚ -โ”‚ 1. Load subtitle file โ”‚ -โ”‚ 2. Submit job to microservice โ”‚ -โ”‚ 3. Poll for completion โ”‚ -โ”‚ 4. Save translated subtitle โ”‚ -โ”‚ 5. Log to history โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - -### How to Trigger Translation - -#### Method 1: Via Bazarr UI (Recommended) -1. Go to Series/Movies in Bazarr -2. Click on an episode/movie -3. Look at the subtitles section -4. Click on an existing subtitle file (e.g., English) -5. Select "Translate" from the actions menu -6. Choose the target language -7. Click confirm - -#### Method 2: Via API -```bash -curl -X PATCH "http://localhost:6767/api/subtitles/subtitles" \ - -H "X-API-KEY: your-api-key" \ - -F "action=translate" \ - -F "language=hu" \ - -F "path=/path/to/subtitle.en.srt" \ - -F "type=episode" \ - -F "id=12345" -``` - -### Automatic Translation (NOT Currently Implemented) - -There is currently **no automatic translation** in Bazarr. To implement this, you would need to: - -1. **Post-download hook**: Add translation to the subtitle download pipeline -2. **Scheduled task**: Create a scheduler job to translate missing languages -3. **Language profile enhancement**: Add "auto-translate" option to language profiles - ---- - -## Relevant Files Reference - -### Configuration -- [`bazarr/app/config.py:186-197`](bazarr/app/config.py:186) - Translator settings validators - -### Backend API -- [`bazarr/api/translator/translator.py`](bazarr/api/translator/translator.py) - Translator API endpoints (status, jobs, config) -- [`bazarr/api/subtitles/subtitles.py:170-181`](bazarr/api/subtitles/subtitles.py:170) - Translate action handler -- [`bazarr/api/system/settings.py`](bazarr/api/system/settings.py) - Settings save endpoint - -### Translation Services -- [`bazarr/subtitles/tools/translate/main.py`](bazarr/subtitles/tools/translate/main.py) - Main translation entry point -- [`bazarr/subtitles/tools/translate/services/translator_factory.py`](bazarr/subtitles/tools/translate/services/translator_factory.py) - Translator factory -- [`bazarr/subtitles/tools/translate/services/openrouter_translator.py`](bazarr/subtitles/tools/translate/services/openrouter_translator.py) - AI Subtitle Translator service - -### Frontend -- [`frontend/src/pages/Settings/Subtitles/index.tsx:534-655`](frontend/src/pages/Settings/Subtitles/index.tsx:534) - Translator settings UI -- [`frontend/src/components/TranslatorStatus.tsx`](frontend/src/components/TranslatorStatus.tsx) - Status panel component -- [`frontend/src/apis/hooks/translator.ts`](frontend/src/apis/hooks/translator.ts) - API hooks for translator - ---- - -## Recommended Fixes Summary - -### Immediate Fixes - -1. **Add `maxConcurrent` to translation payload** (1 line change) - - File: `bazarr/subtitles/tools/translate/services/openrouter_translator.py` - - Add: `"maxConcurrent": settings.translator.openrouter_max_concurrent` - -2. **Update Status Panel display** - - Show Bazarr's configured values, with clarification label - - Alternative: Sync config to microservice on save - -### Future Enhancements - -1. **Add automatic translation option** - - Post-download hook for newly downloaded subtitles - - Option in language profiles - -2. **Real-time config sync** - - Push config changes to microservice immediately - - Keep microservice in sync with Bazarr settings \ No newline at end of file diff --git a/docs/FORK_MAINTENANCE.md b/docs/FORK_MAINTENANCE.md index f9aba4cdfc..3f22a05033 100644 --- a/docs/FORK_MAINTENANCE.md +++ b/docs/FORK_MAINTENANCE.md @@ -1,285 +1,88 @@ -# Bazarr LavX Fork - Maintenance Guide +# Bazarr+ Maintenance Guide -This document describes the automated workflow for maintaining this fork of [Bazarr](https://github.com/morpheus65535/bazarr). +Bazarr+ is a hard fork of [Bazarr](https://github.com/morpheus65535/bazarr). It shares the original codebase as a starting point but is developed independently. There is no automatic upstream synchronization. -## Overview +## Relationship with Upstream -This fork contains custom modifications (primarily the OpenSubtitles.org web scraper provider) that are automatically kept in sync with the upstream Bazarr repository. The workflow handles: +Bazarr+ may selectively cherry-pick bug fixes from upstream when relevant, but does not merge upstream releases wholesale. The codebases have diverged significantly in security model, UI, features, and architecture. -1. **Upstream Synchronization** - Daily automatic merging of upstream changes -2. **Conflict Resolution** - Automatic PR creation when merge conflicts occur -3. **Docker Builds** - Automated multi-architecture Docker image builds -4. **Publishing** - Images published to GitHub Container Registry +**When cherry-picking from upstream:** +1. Evaluate whether the fix applies to Bazarr+ (upstream may fix things we've already addressed differently) +2. Cherry-pick individual commits: `git cherry-pick ` +3. Review for conflicts with fork-specific code (security hardening, telemetry removal, UI changes) +4. Test thoroughly before merging -## Architecture - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ GitHub Actions Workflows โ”‚ -โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค -โ”‚ โ”‚ -โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”‚ -โ”‚ โ”‚ sync-upstream.yml โ”‚ โ”‚ build-docker.yml โ”‚ โ”‚ -โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ โ”‚ -โ”‚ โ”‚ โ€ข Daily 4 AM UTC โ”‚โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€>โ”‚ โ€ข Build frontend โ”‚ โ”‚ -โ”‚ โ”‚ โ€ข Fetch upstream โ”‚ trigger โ”‚ โ€ข Build Docker image โ”‚ โ”‚ -โ”‚ โ”‚ โ€ข Auto merge โ”‚ โ”‚ โ€ข Push to ghcr.io โ”‚ โ”‚ -โ”‚ โ”‚ โ€ข Conflict PR โ”‚ โ”‚ โ€ข multi-arch (amd64/arm64) โ”‚ โ”‚ -โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ -โ”‚ โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - -## Workflows - -### 1. Upstream Sync (`sync-upstream.yml`) - -**Schedule:** Daily at 4:00 AM UTC (5:00 AM Budapest time) - -**Triggers:** -- Scheduled cron job -- Manual dispatch from GitHub Actions UI +## Versioning -**Process:** -1. Fetches latest commits from `morpheus65535/bazarr:master` -2. Compares with current fork -3. Attempts automatic merge -4. If successful: pushes changes and triggers Docker build -5. If conflicts: creates a PR with `sync-conflict` label +Bazarr+ uses independent semantic versioning, starting at v2.0.0. Versions are not tied to upstream Bazarr release numbers. -**Manual Trigger:** -```bash -# Via GitHub CLI -gh workflow run sync-upstream.yml +``` +v{major}.{minor}.{patch} -# With force sync (even if no new commits) -gh workflow run sync-upstream.yml -f force_sync=true +Example: v2.0.0, v2.1.0, v2.0.1 ``` -### 2. Docker Build (`build-docker.yml`) +- Patch (v2.0.1): bug fixes +- Minor (v2.1.0): new features, backwards-compatible +- Major (v3.0.0): breaking changes + +## Docker Build (`build-docker.yml`) **Triggers:** -- Push to `main` branch -- Called by sync workflow after successful merge +- Push to `master` branch - Manual dispatch - Tag creation (for releases) **Output:** - `ghcr.io/lavx/bazarr:latest` - Latest build -- `ghcr.io/lavx/bazarr:vX.Y.Z-lavx.YYYYMMDD` - Versioned build +- `ghcr.io/lavx/bazarr:X.Y.Z` - Versioned build - `ghcr.io/lavx/bazarr:sha-XXXXXXX` - Git SHA reference -## Using the Docker Image +## Branch Model -### Quick Start - -```bash -docker run -d \ - --name bazarr \ - -p 6767:6767 \ - -v /path/to/config:/config \ - -v /path/to/movies:/movies \ - -v /path/to/tv:/tv \ - -e PUID=1000 \ - -e PGID=1000 \ - -e TZ=Europe/Budapest \ - ghcr.io/lavx/bazarr:latest -``` - -### Docker Compose - -Create a `docker-compose.yml`: - -```yaml -services: - bazarr: - image: ghcr.io/lavx/bazarr:latest - container_name: bazarr - restart: unless-stopped - ports: - - "6767:6767" - environment: - - PUID=1000 - - PGID=1000 - - TZ=Europe/Budapest - volumes: - - ./config:/config - - /path/to/movies:/movies - - /path/to/tv:/tv -``` - -Then run: -```bash -docker compose up -d -``` - -### Environment Variables - -| Variable | Default | Description | -|----------|---------|-------------| -| `PUID` | `1000` | User ID for file permissions | -| `PGID` | `1000` | Group ID for file permissions | -| `TZ` | `UTC` | Timezone (e.g., `Europe/Budapest`) | - -## Handling Merge Conflicts - -When upstream changes conflict with fork modifications: - -1. **Notification:** A PR is automatically created with the `sync-conflict` label -2. **Review:** Check the PR for conflict markers -3. **Fix locally:** - ```bash - git fetch origin sync/upstream-XXXXXXXX - git checkout sync/upstream-XXXXXXXX - # Resolve conflicts in your editor - git add . - git commit -m "Resolve merge conflicts" - git push origin sync/upstream-XXXXXXXX - ``` -4. **Merge:** Merge the PR via GitHub UI -5. **Build:** Docker build will trigger automatically +- `master` contains stable releases +- `development` is the integration branch where new features land +- Feature branches are created from `development` and merged back via PR ## Fork-Specific Files -These files are unique to this fork and should be preserved during merges: +Key files that define Bazarr+ and differentiate it from upstream: | File | Purpose | |------|---------| | `custom_libs/subliminal_patch/providers/opensubtitles_scraper.py` | OpenSubtitles.org scraper mixin | | `custom_libs/subliminal_patch/providers/opensubtitles.py` | Modified provider with scraper support | -| `opensubtitles-scraper/` | Git submodule - web scraper service | +| `opensubtitles-scraper/` | Git submodule: web scraper service | +| `ai-subtitle-translator/` | Git submodule: AI translator service | | `package_info` | Fork identification (shown in System Status) | -| `bazarr/app/check_update.py` | Modified to use fork's releases | -| `.github/workflows/sync-upstream.yml` | Upstream sync workflow | +| `bazarr/app/check_update.py` | Uses fork's releases, not upstream | | `.github/workflows/build-docker.yml` | Docker build workflow | -| `Dockerfile` | Production Docker image | +| `Dockerfile` | Production Docker image (Python 3.14) | | `docker-compose.yml` | User deployment template | -| `.gitattributes` | Merge conflict protection rules | - -## OpenSubtitles Scraper Service +| `bazarr/utilities/analytics.py` | Deleted (contained GA4 + UA telemetry) | -This fork includes the [OpenSubtitles Scraper](https://github.com/LavX/opensubtitles-scraper) as a git submodule. The scraper is a standalone service that provides web scraping capabilities for OpenSubtitles.org. - -### Architecture - -``` -โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” HTTP API โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” -โ”‚ Bazarr โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€> โ”‚ OpenSubtitles Scraper โ”‚ -โ”‚ (LavX Fork) โ”‚ โ”‚ (Port 8765) โ”‚ -โ”‚ โ”‚ <โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ โ”‚ โ”‚ -โ”‚ Uses provider: โ”‚ JSON Response โ”‚ Scrapes: โ”‚ -โ”‚ opensubtitles.org โ”‚ โ”‚ - opensubtitles.org โ”‚ -โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ -``` - -### Docker Compose Deployment - -The `docker-compose.yml` includes both services: - -```yaml -services: - opensubtitles-scraper: - image: ghcr.io/lavx/opensubtitles-scraper:latest - ports: - - "8765:8765" - - bazarr: - image: ghcr.io/lavx/bazarr:latest - depends_on: - - opensubtitles-scraper - environment: - - OPENSUBTITLES_SCRAPER_URL=http://opensubtitles-scraper:8765 -``` +## Auto-Update Behavior -### Updating the Scraper Submodule +### Docker: Auto-Update is Disabled -To update the scraper to the latest version: +The Docker image runs with `--no-update` flag. Update by pulling new images: ```bash -cd opensubtitles-scraper -git pull origin main -cd .. -git add opensubtitles-scraper -git commit -m "Update opensubtitles-scraper submodule" -git push -``` - -## Versioning - -This fork uses a versioning scheme that combines upstream version with fork identifier: - -``` -{upstream_version}-lavx.{date} - -Example: v1.5.3-lavx.20241214 +docker compose pull +docker compose up -d ``` -This makes it clear: -- Which upstream version the build is based on -- When the fork build was created -- That it contains fork-specific modifications +### Release Repository -## Auto-Update Behavior - -### Important: Auto-Update is Disabled in Docker - -The Docker image runs with `--no-update` flag to prevent Bazarr's built-in update mechanism from overwriting your fork modifications. **This is intentional.** - -### How Updates Work - -| Scenario | Behavior | -|----------|----------| -| **Docker Container** | Auto-update disabled; use new Docker image for updates | -| **Manual Installation** | Auto-update can be enabled, but will pull from this fork | -| **Release Info in UI** | Shows releases from this fork (LavX/bazarr) | - -### Release Repository Configuration - -The fork is configured to check releases from `LavX/bazarr` instead of upstream. This is controlled by: +The fork checks releases from `LavX/bazarr`: ```python # In bazarr/app/check_update.py RELEASES_REPO = os.environ.get('BAZARR_RELEASES_REPO', 'LavX/bazarr') ``` -To change the release source (e.g., for debugging), set the environment variable: - -```yaml -# docker-compose.yml -environment: - - BAZARR_RELEASES_REPO=morpheus65535/Bazarr # Use upstream releases -``` - -### Updating Docker Containers - -To update to a new version: - -```bash -# Pull the latest image -docker compose pull - -# Recreate the container -docker compose up -d - -# Or in one command -docker compose up -d --pull always -``` - -### Why Docker Doesn't Auto-Update - -1. **Preservation of modifications**: Auto-update would download vanilla Bazarr, losing the OpenSubtitles scraper -2. **Immutable containers**: Docker best practices recommend replacing containers rather than modifying them -3. **Reproducibility**: Pinned versions ensure consistent behavior -4. **Rollback capability**: Easy to rollback by pulling a specific tag - ## Troubleshooting -### Sync Workflow Fails - -1. Check the workflow logs in GitHub Actions -2. Verify upstream repository is accessible -3. Check if there are unresolved conflicts from previous sync - ### Docker Build Fails 1. Check if frontend build succeeded @@ -296,17 +99,8 @@ echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin docker pull ghcr.io/lavx/bazarr:latest ``` -## Contributing - -When making changes to fork-specific files: - -1. Test changes locally first -2. Ensure changes don't conflict with upstream structure -3. Document any new environment variables or features -4. Update this documentation if workflow changes - ## Related Links -- [Upstream Bazarr Repository](https://github.com/morpheus65535/bazarr) +- [Upstream Bazarr Repository](https://github.com/morpheus65535/bazarr) (original, not synced) - [GitHub Container Registry](https://ghcr.io/lavx/bazarr) -- [Bazarr Wiki](https://wiki.bazarr.media) \ No newline at end of file +- [Contributing Guide](../CONTRIBUTING.md) diff --git a/frontend/.eslintrc.json b/frontend/.eslintrc.json deleted file mode 100644 index f1418bfdf5..0000000000 --- a/frontend/.eslintrc.json +++ /dev/null @@ -1,70 +0,0 @@ -{ - "rules": { - "no-console": "error", - "camelcase": "warn", - "no-restricted-imports": [ - "error", - { - "patterns": ["..*"] - } - ], - "simple-import-sort/imports": "error", - "@typescript-eslint/explicit-module-boundary-types": "off", - "@typescript-eslint/no-empty-function": "warn", - "@typescript-eslint/no-empty-interface": "off", - "@typescript-eslint/no-unused-vars": "warn" - }, - "extends": [ - "eslint:recommended", - "plugin:react-hooks/recommended", - "plugin:@typescript-eslint/recommended" - ], - "plugins": ["testing-library", "simple-import-sort", "react-refresh"], - "overrides": [ - { - "files": [ - "**/__tests__/**/*.[jt]s?(x)", - "**/?(*.)+(spec|test).[jt]s?(x)" - ], - "extends": ["plugin:testing-library/react"] - }, - { - "files": ["*.ts", "*.tsx"], - "rules": { - "simple-import-sort/imports": [ - "error", - { - "groups": [ - [ - // React Packages - "^react", - // Mantine Packages - "^@mantine/", - // Vendor Packages - "^(\\w|@\\w)", - // Side Effect Imports - "^\\u0000", - // Internal Packages - "^@/\\w", - // Parent Imports - "^\\.\\.(?!/?$)", - "^\\.\\./?$", - // Relative Imports - "^\\./(?=.*/)(?!/?$)", - "^\\.(?!/?$)", - "^\\./?$", - // Style Imports - "^.+\\.?(css)$" - ] - ] - } - ] - } - } - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "sourceType": "module", - "ecmaVersion": "latest" - } -} diff --git a/frontend/.husky/pre-commit b/frontend/.husky/pre-commit index 71a781c99a..b47088403d 100755 --- a/frontend/.husky/pre-commit +++ b/frontend/.husky/pre-commit @@ -1,6 +1,3 @@ -#!/bin/sh -. "$(dirname "$0")/_/husky.sh" - [ -n "$CI" ] && exit 0 cd frontend || exit diff --git a/frontend/README.md b/frontend/README.md index 5fcd3d4816..8539c40437 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -14,7 +14,7 @@ 1. Clone or download this repository ``` - $ git clone https://github.com/morpheus65535/bazarr.git + $ git clone https://github.com/LavX/bazarr.git $ cd bazarr/frontend ``` diff --git a/frontend/eslint.config.mjs b/frontend/eslint.config.mjs new file mode 100644 index 0000000000..136e64e304 --- /dev/null +++ b/frontend/eslint.config.mjs @@ -0,0 +1,89 @@ +import js from "@eslint/js"; +import tseslint from "typescript-eslint"; +import reactHooks from "eslint-plugin-react-hooks"; +import reactRefresh from "eslint-plugin-react-refresh"; +import simpleImportSort from "eslint-plugin-simple-import-sort"; +import testingLibrary from "eslint-plugin-testing-library"; +import globals from "globals"; + +export default tseslint.config( + { + ignores: [ + "dist", + "build", + "coverage", + "node_modules", + "dev-dist", + ".husky", + "eslint.config.mjs", + "vite.config.ts", + ], + }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + plugins: { + "react-hooks": reactHooks, + "react-refresh": reactRefresh.default || reactRefresh, + "simple-import-sort": simpleImportSort, + }, + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + }, + }, + rules: { + ...reactHooks.configs.recommended.rules, + "no-console": "error", + camelcase: "warn", + "no-restricted-imports": [ + "error", + { + patterns: ["..*"], + }, + ], + "simple-import-sort/imports": "error", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-empty-function": "warn", + "@typescript-eslint/no-empty-interface": "off", + "@typescript-eslint/no-unused-vars": "warn", + "@typescript-eslint/no-empty-object-type": "off", + "@typescript-eslint/no-unused-expressions": "off", + "no-useless-assignment": "off", + "react-hooks/set-state-in-effect": "off", + "react-hooks/refs": "off", + "react-hooks/static-components": "off", + "react-hooks/incompatible-library": "off", + }, + }, + { + files: ["**/*.ts", "**/*.tsx"], + rules: { + "simple-import-sort/imports": [ + "error", + { + groups: [ + [ + "^react", + "^@mantine/", + "^(\\w|@\\w)", + "^\\u0000", + "^@/\\w", + "^\\.\\.(?!/?$)", + "^\\.\\./?$", + "^\\./(?=.*/)(?!/?$)", + "^\\.(?!/?$)", + "^\\./?$", + "^.+\\.?(css)$", + ], + ], + }, + ], + }, + }, + { + files: ["**/__tests__/**/*.[jt]s?(x)", "**/?(*.)+(spec|test).[jt]s?(x)"], + ...testingLibrary.configs["flat/react"], + }, +); diff --git a/frontend/index.html b/frontend/index.html index 46f5375a87..0d79f37e5b 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -15,7 +15,7 @@ href="./images/maskable-icon-512x512.png" color="#FFFFFF" /> - + =24.0.0" } }, "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", "dev": true, - "license": "ISC" + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.8.1.tgz", + "integrity": "sha512-MvRz1nCqW0fsy8Qz4dnLIvhOlMzqDVBabZx6lH+YywFDdjXhMY37SmpV1XFX3JzG5GWHn63j6HX6QPr3lZXHvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.6" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" }, "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.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", "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" }, @@ -128,9 +178,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "dev": true, "license": "MIT", "engines": { @@ -168,21 +218,15 @@ "url": "https://opencollective.com/babel" } }, - "node_modules/@babel/core/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true - }, "node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" @@ -192,26 +236,26 @@ } }, "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" } }, "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", @@ -222,18 +266,18 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.0.tgz", - "integrity": "sha512-vSGCvMecvFCd/BdpGlhpXYNhhC4ccxyvQWpbGL4CWbvfEoLFWUZuSuf7s9Aw70flgQF+6vptvgK2IfOnKlRmBg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/helper-replace-supers": "^7.26.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/traverse": "^7.27.0", + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "engines": { @@ -244,14 +288,14 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.0.tgz", - "integrity": "sha512-fO8l08T76v48BhpNRW/nQ0MxfnSdoSKUJBMjubOAYffsVuGG5qOfMq7N6Es7UJvi7Y8goXXo07EfcHZXDPuELQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "regexpu-core": "^6.2.0", + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", "semver": "^6.3.1" }, "engines": { @@ -262,22 +306,47 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz", - "integrity": "sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw==", + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.7.tgz", + "integrity": "sha512-6Fqi8MtQ/PweQ9xvux65emkLQ83uB+qAVtfHkC9UodyHMIZdxNI01HjLCLUtybElp2KY2XNE0nOgyP1E1vXw9w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" + "resolve": "^1.22.11" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", @@ -289,43 +358,43 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", - "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, "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" @@ -335,22 +404,22 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", - "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true, "license": "MIT", "engines": { @@ -358,15 +427,15 @@ } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", - "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-wrap-function": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -376,15 +445,15 @@ } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", - "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.26.5" + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -394,14 +463,14 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", - "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -418,9 +487,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, "license": "MIT", "engines": { @@ -438,42 +507,42 @@ } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", - "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", + "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", - "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz", + "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", - "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.2" + "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" @@ -483,14 +552,14 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", - "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", + "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -500,13 +569,13 @@ } }, "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", - "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -516,13 +585,13 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", - "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -532,15 +601,15 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", - "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -550,14 +619,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", - "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", + "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -580,13 +649,13 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", - "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", + "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -596,13 +665,13 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", - "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -629,13 +698,13 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", - "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -645,15 +714,15 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.26.8.tgz", - "integrity": "sha512-He9Ej2X7tNf2zdKMAGOsmg2MrFc+hfoAhd3po4cWfo/NWjzEAKa0oQruj1ROVUdl0e6fb6/kE/G3SSxE0lRJOg==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", + "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5", - "@babel/helper-remap-async-to-generator": "^7.25.9", - "@babel/traverse": "^7.26.8" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.29.0" }, "engines": { "node": ">=6.9.0" @@ -663,15 +732,15 @@ } }, "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", - "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", + "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-remap-async-to-generator": "^7.25.9" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -681,13 +750,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz", - "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -697,13 +766,13 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.0.tgz", - "integrity": "sha512-u1jGphZ8uDI2Pj/HJj6YQ6XQLZCNjOlprjxB5SVz6rq2T6SwAR+CdrWK0CP7F+9rDVMXdB0+r6Am5G5aobOjAQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", + "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -713,14 +782,14 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", - "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", + "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -730,14 +799,14 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", - "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", + "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -747,18 +816,18 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", - "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", + "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/traverse": "^7.25.9", - "globals": "^11.1.0" + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -768,14 +837,14 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", - "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", + "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/template": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/template": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -785,13 +854,14 @@ } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", - "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -801,14 +871,14 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", - "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", + "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -818,13 +888,13 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", - "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -834,14 +904,14 @@ } }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", - "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -851,13 +921,30 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", - "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-explicit-resource-management": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", + "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -867,13 +954,13 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", - "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", + "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -883,13 +970,13 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", - "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -899,14 +986,14 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.26.9.tgz", - "integrity": "sha512-Hry8AusVm8LW5BVFgiyUReuoGzPUpdHQQqJY5bZnbbf+ngOHWuCuYFKw/BqaaWlvEUrF91HMhDtEaI1hZzNbLg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -916,15 +1003,15 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", - "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -934,13 +1021,13 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", - "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", + "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -950,13 +1037,13 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", - "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -966,13 +1053,13 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", - "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", + "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -982,13 +1069,13 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", - "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -998,14 +1085,14 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", - "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1015,14 +1102,14 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", - "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1032,16 +1119,16 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", - "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", + "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.29.0" }, "engines": { "node": ">=6.9.0" @@ -1051,14 +1138,14 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", - "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1068,14 +1155,14 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", - "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1085,13 +1172,13 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", - "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1101,13 +1188,13 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.26.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz", - "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", + "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1117,13 +1204,13 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", - "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", + "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1133,15 +1220,17 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", - "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", + "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9" + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1151,14 +1240,14 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", - "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1168,13 +1257,13 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", - "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", + "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1184,14 +1273,14 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", - "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", + "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1201,13 +1290,13 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", - "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1217,14 +1306,14 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", - "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", + "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1234,15 +1323,15 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", - "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", + "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1252,13 +1341,13 @@ } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", - "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1300,14 +1389,13 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.0.tgz", - "integrity": "sha512-LX/vCajUJQDqE7Aum/ELUMZAY19+cDpghxrnyt5I1tV6X5PyC86AOoWXWFYFeIvauyeSA6/ktn4tQVn/3ZifsA==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", + "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5", - "regenerator-transform": "^0.15.2" + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1317,14 +1405,14 @@ } }, "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", - "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", + "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1334,13 +1422,13 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", - "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1350,13 +1438,13 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", - "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1366,14 +1454,14 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", - "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", + "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1383,13 +1471,13 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", - "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1399,13 +1487,13 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.26.8", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.26.8.tgz", - "integrity": "sha512-OmGDL5/J0CJPJZTHZbi2XpO0tyT2Ia7fzpW5GURwdtp2X3fMmN8au/ej6peC/T33/+CRiIpA8Krse8hFGVmT5Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1415,13 +1503,13 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.27.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.0.tgz", - "integrity": "sha512-+LLkxA9rKJpNoGsbLnAgOCdESl73vwYn+V6b+5wHbrE7OGKVDPHIQvbFSzqE6rwqaCw2RE+zdJrlLkcf8YOA0w==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1431,13 +1519,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", - "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1447,14 +1535,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", - "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", + "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1464,14 +1552,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", - "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -1481,14 +1569,14 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", - "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", + "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -1498,80 +1586,81 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.26.9", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.9.tgz", - "integrity": "sha512-vX3qPGE8sEKEAZCWk05k3cpTAE3/nOYca++JA+Rd0z2NCNzabmYvEiSShKzm10zdquOIAVXsy2Ei/DTW34KlKQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.29.0.tgz", + "integrity": "sha512-fNEdfc0yi16lt6IZo2Qxk3knHVdfMYX33czNb4v8yWhemoBhibCpQK/uYHtSKIiO+p/zd3+8fYVXhQdOVV608w==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.26.8", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-plugin-utils": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", + "@babel/compat-data": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.28.5", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.6", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.26.0", - "@babel/plugin-syntax-import-attributes": "^7.26.0", + "@babel/plugin-syntax-import-assertions": "^7.28.6", + "@babel/plugin-syntax-import-attributes": "^7.28.6", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.25.9", - "@babel/plugin-transform-async-generator-functions": "^7.26.8", - "@babel/plugin-transform-async-to-generator": "^7.25.9", - "@babel/plugin-transform-block-scoped-functions": "^7.26.5", - "@babel/plugin-transform-block-scoping": "^7.25.9", - "@babel/plugin-transform-class-properties": "^7.25.9", - "@babel/plugin-transform-class-static-block": "^7.26.0", - "@babel/plugin-transform-classes": "^7.25.9", - "@babel/plugin-transform-computed-properties": "^7.25.9", - "@babel/plugin-transform-destructuring": "^7.25.9", - "@babel/plugin-transform-dotall-regex": "^7.25.9", - "@babel/plugin-transform-duplicate-keys": "^7.25.9", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-dynamic-import": "^7.25.9", - "@babel/plugin-transform-exponentiation-operator": "^7.26.3", - "@babel/plugin-transform-export-namespace-from": "^7.25.9", - "@babel/plugin-transform-for-of": "^7.26.9", - "@babel/plugin-transform-function-name": "^7.25.9", - "@babel/plugin-transform-json-strings": "^7.25.9", - "@babel/plugin-transform-literals": "^7.25.9", - "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", - "@babel/plugin-transform-member-expression-literals": "^7.25.9", - "@babel/plugin-transform-modules-amd": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.26.3", - "@babel/plugin-transform-modules-systemjs": "^7.25.9", - "@babel/plugin-transform-modules-umd": "^7.25.9", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-new-target": "^7.25.9", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.26.6", - "@babel/plugin-transform-numeric-separator": "^7.25.9", - "@babel/plugin-transform-object-rest-spread": "^7.25.9", - "@babel/plugin-transform-object-super": "^7.25.9", - "@babel/plugin-transform-optional-catch-binding": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9", - "@babel/plugin-transform-private-methods": "^7.25.9", - "@babel/plugin-transform-private-property-in-object": "^7.25.9", - "@babel/plugin-transform-property-literals": "^7.25.9", - "@babel/plugin-transform-regenerator": "^7.25.9", - "@babel/plugin-transform-regexp-modifiers": "^7.26.0", - "@babel/plugin-transform-reserved-words": "^7.25.9", - "@babel/plugin-transform-shorthand-properties": "^7.25.9", - "@babel/plugin-transform-spread": "^7.25.9", - "@babel/plugin-transform-sticky-regex": "^7.25.9", - "@babel/plugin-transform-template-literals": "^7.26.8", - "@babel/plugin-transform-typeof-symbol": "^7.26.7", - "@babel/plugin-transform-unicode-escapes": "^7.25.9", - "@babel/plugin-transform-unicode-property-regex": "^7.25.9", - "@babel/plugin-transform-unicode-regex": "^7.25.9", - "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.29.0", + "@babel/plugin-transform-async-to-generator": "^7.28.6", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.28.6", + "@babel/plugin-transform-class-properties": "^7.28.6", + "@babel/plugin-transform-class-static-block": "^7.28.6", + "@babel/plugin-transform-classes": "^7.28.6", + "@babel/plugin-transform-computed-properties": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-dotall-regex": "^7.28.6", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.29.0", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-explicit-resource-management": "^7.28.6", + "@babel/plugin-transform-exponentiation-operator": "^7.28.6", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.28.6", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.28.6", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.28.6", + "@babel/plugin-transform-modules-systemjs": "^7.29.0", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.29.0", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.28.6", + "@babel/plugin-transform-numeric-separator": "^7.28.6", + "@babel/plugin-transform-object-rest-spread": "^7.28.6", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.28.6", + "@babel/plugin-transform-optional-chaining": "^7.28.6", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/plugin-transform-private-methods": "^7.28.6", + "@babel/plugin-transform-private-property-in-object": "^7.28.6", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.29.0", + "@babel/plugin-transform-regexp-modifiers": "^7.28.6", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.28.6", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.28.6", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.28.6", "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.11.0", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.40.0", + "babel-plugin-polyfill-corejs2": "^0.4.15", + "babel-plugin-polyfill-corejs3": "^0.14.0", + "babel-plugin-polyfill-regenerator": "^0.6.6", + "core-js-compat": "^3.48.0", "semver": "^6.3.1" }, "engines": { @@ -1609,33 +1698,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.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", - "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.3", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", "debug": "^4.3.1" }, "engines": { @@ -1643,14 +1732,14 @@ } }, "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" @@ -1666,69 +1755,25 @@ "node": ">=18" } }, - "node_modules/@bufbuild/protobuf": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.2.5.tgz", - "integrity": "sha512-/g5EzJifw5GF8aren8wZ/G5oMuPoGeS6MQD3ca8ddcvdXR5UELUfdTZITCGNhNXynY/AYl3Z4plmxdj/tRl/hQ==", - "dev": true, - "license": "(Apache-2.0 AND BSD-3-Clause)" - }, - "node_modules/@bundled-es-modules/cookie": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz", - "integrity": "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==", - "dev": true, - "license": "ISC", - "dependencies": { - "cookie": "^0.7.2" - } - }, - "node_modules/@bundled-es-modules/cookie/node_modules/cookie": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", - "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/@bundled-es-modules/statuses": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz", - "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==", - "dev": true, - "license": "ISC", - "dependencies": { - "statuses": "^2.0.1" - } - }, - "node_modules/@bundled-es-modules/tough-cookie": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz", - "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==", - "dev": true, - "license": "ISC", "dependencies": { - "@types/tough-cookie": "^4.0.5", - "tough-cookie": "^4.1.4" + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" } }, - "node_modules/@bundled-es-modules/tough-cookie/node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "node_modules/@bufbuild/protobuf": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-2.11.0.tgz", + "integrity": "sha512-sBXGT13cpmPR5BMgHE6UEEfEaShh5Ror6rfN3yEK5si7QVrtZg8LEPQb0VVhiLRUslD2yLnXtnRzG035J/mZXQ==", "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" - }, - "engines": { - "node": ">=6" - } + "license": "(Apache-2.0 AND BSD-3-Clause)" }, "node_modules/@canvas/image-data": { "version": "1.0.0", @@ -1738,9 +1783,9 @@ "license": "MIT" }, "node_modules/@csstools/color-helpers": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.0.1.tgz", - "integrity": "sha512-MKtmkA0BX87PKaO1NFRTFH+UnkgnmySQOvNxJubsadusqPEC2aJ9MOQiMceZJJ6oitUl/i0L6u0M1IrmAOmgBA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", "dev": true, "funding": [ { @@ -1754,13 +1799,13 @@ ], "license": "MIT-0", "engines": { - "node": ">=18" + "node": ">=20.19.0" } }, "node_modules/@csstools/css-calc": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.1.tgz", - "integrity": "sha512-rL7kaUnTkL9K+Cvo2pnCieqNpTKgQzy5f+N+5Iuko9HAoasP+xgprVh7KN/MaJVvVL1l0EzQq2MoqBHKSrDrag==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.1.1.tgz", + "integrity": "sha512-HJ26Z/vmsZQqs/o3a6bgKslXGFAungXGbinULZO3eMsOyNJHeBBZfup5FiZInOghgoM4Hwnmw+OgbJCNg1wwUQ==", "dev": true, "funding": [ { @@ -1774,17 +1819,17 @@ ], "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-color-parser": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.0.7.tgz", - "integrity": "sha512-nkMp2mTICw32uE5NN+EsJ4f5N+IGFeCFu4bGpiKgb2Pq/7J/MpyLBeQ5ry4KKtRFZaYs6sTmcMYrSRIyj5DFKA==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.0.2.tgz", + "integrity": "sha512-0GEfbBLmTFf0dJlpsNU7zwxRIH0/BGEMuXLTCvFYxuL1tNhqzTbtnFICyJLTNK4a+RechKP75e7w42ClXSnJQw==", "dev": true, "funding": [ { @@ -1798,21 +1843,21 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^5.0.1", - "@csstools/css-calc": "^2.1.1" + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.1.1" }, "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.4", - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" } }, "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz", - "integrity": "sha512-Up7rBoV77rv29d3uKHUIVubz1BTcgyUK72IvCQAbfbMv584xHcGKCKbWh7i8hPrRJ7qU4Y8IO3IY9m+iTB7P3A==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", "dev": true, "funding": [ { @@ -1826,16 +1871,41 @@ ], "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20.19.0" }, "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.3" + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.2.tgz", + "integrity": "sha512-5GkLzz4prTIpoyeUiIu3iV6CSG3Plo7xRVOFPKI7FVEJ3mZ0A8SwK0XU3Gl7xAkiQ+mDyam+NNp875/C5y+jSA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } } }, "node_modules/@csstools/css-tokenizer": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.3.tgz", - "integrity": "sha512-UJnjoFsmxfKUdNYdWgOB0mWUypuLvAfQPH1+pyvRJs6euowbFkFC6P13w1l8mJyi3vxYMxc9kld5jZEGRQs6bw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", "dev": true, "funding": [ { @@ -1849,13 +1919,13 @@ ], "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20.19.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", "dev": true, "license": "MIT", "optional": true, @@ -1864,9 +1934,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.2.tgz", - "integrity": "sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", "cpu": [ "ppc64" ], @@ -1881,9 +1951,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.2.tgz", - "integrity": "sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", "cpu": [ "arm" ], @@ -1898,9 +1968,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.2.tgz", - "integrity": "sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", "cpu": [ "arm64" ], @@ -1915,9 +1985,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.2.tgz", - "integrity": "sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", "cpu": [ "x64" ], @@ -1932,9 +2002,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.2.tgz", - "integrity": "sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", "cpu": [ "arm64" ], @@ -1949,9 +2019,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.2.tgz", - "integrity": "sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", "cpu": [ "x64" ], @@ -1966,9 +2036,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.2.tgz", - "integrity": "sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", "cpu": [ "arm64" ], @@ -1983,9 +2053,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.2.tgz", - "integrity": "sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", "cpu": [ "x64" ], @@ -2000,9 +2070,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.2.tgz", - "integrity": "sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", "cpu": [ "arm" ], @@ -2017,9 +2087,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.2.tgz", - "integrity": "sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", "cpu": [ "arm64" ], @@ -2034,9 +2104,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.2.tgz", - "integrity": "sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", "cpu": [ "ia32" ], @@ -2051,9 +2121,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.2.tgz", - "integrity": "sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", "cpu": [ "loong64" ], @@ -2068,9 +2138,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.2.tgz", - "integrity": "sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", "cpu": [ "mips64el" ], @@ -2085,9 +2155,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.2.tgz", - "integrity": "sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", "cpu": [ "ppc64" ], @@ -2102,9 +2172,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.2.tgz", - "integrity": "sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", "cpu": [ "riscv64" ], @@ -2119,9 +2189,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.2.tgz", - "integrity": "sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", "cpu": [ "s390x" ], @@ -2136,9 +2206,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.2.tgz", - "integrity": "sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", "cpu": [ "x64" ], @@ -2153,9 +2223,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.2.tgz", - "integrity": "sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", "cpu": [ "arm64" ], @@ -2170,9 +2240,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.2.tgz", - "integrity": "sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", "cpu": [ "x64" ], @@ -2187,9 +2257,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.2.tgz", - "integrity": "sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", "cpu": [ "arm64" ], @@ -2204,9 +2274,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.2.tgz", - "integrity": "sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", "cpu": [ "x64" ], @@ -2220,10 +2290,27 @@ "node": ">=18" } }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.2.tgz", - "integrity": "sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", "cpu": [ "x64" ], @@ -2238,9 +2325,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.2.tgz", - "integrity": "sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", "cpu": [ "arm64" ], @@ -2255,9 +2342,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.2.tgz", - "integrity": "sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", "cpu": [ "ia32" ], @@ -2272,9 +2359,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.2.tgz", - "integrity": "sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", "cpu": [ "x64" ], @@ -2289,86 +2376,136 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", - "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "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": { - "eslint-visitor-keys": "^3.3.0" + "eslint-visitor-keys": "^3.4.3" }, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, + "funding": { + "url": "https://opencollective.com/eslint" + }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "node_modules/@eslint/config-array": { + "version": "0.23.3", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", + "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "@eslint/object-schema": "^3.0.3", + "debug": "^4.3.1", + "minimatch": "^10.2.4" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "node_modules/@eslint/config-helpers": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", + "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "type-fest": "^0.20.2" + "@eslint/core": "^1.1.1" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "node_modules/@eslint/core": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", + "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", "dev": true, - "engines": { - "node": ">=10" + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", + "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", "dev": true, + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "eslint": "^10.0.0" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", + "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", + "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.1.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", + "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } } }, "node_modules/@floating-ui/core": { @@ -2424,16 +2561,19 @@ "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, - "node_modules/@fontsource/roboto": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.12.tgz", - "integrity": "sha512-x0o17jvgoSSbS9OZnUX2+xJmVRvVCfeaYJjkS7w62iN7CuJWtMf5vJj8LqgC7ibqIkitOHVW+XssRjgrcHn62g==", - "dev": true + "node_modules/@fontsource-variable/geist": { + "version": "5.2.8", + "resolved": "https://registry.npmjs.org/@fontsource-variable/geist/-/geist-5.2.8.tgz", + "integrity": "sha512-cJ6m9e+8MQ5dCYJsLylfZrgBh6KkG4bOLckB35Tr9J/EqdkEM6QllH5PxqP1dhTvFup+HtMRPuz9xOjxXJggxw==", + "license": "OFL-1.1", + "funding": { + "url": "https://github.com/sponsors/ayuhito" + } }, "node_modules/@fortawesome/fontawesome-common-types": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.1.0.tgz", - "integrity": "sha512-l/BQM7fYntsCI//du+6sEnHOP6a74UixFyOYUyz2DLMXKx+6DEhfR3F2NYGE45XH1JJuIamacb4IZs9S0ZOWLA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-7.2.0.tgz", + "integrity": "sha512-IpR0bER9FY25p+e7BmFH25MZKEwFHTfRAfhOyJubgiDnoJNsSvJ7nigLraHtp4VOG/cy8D7uiV0dLkHOne5Fhw==", "dev": true, "license": "MIT", "engines": { @@ -2441,61 +2581,61 @@ } }, "node_modules/@fortawesome/fontawesome-svg-core": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.1.0.tgz", - "integrity": "sha512-fNxRUk1KhjSbnbuBxlWSnBLKLBNun52ZBTcs22H/xEEzM6Ap81ZFTQ4bZBxVQGQgVY0xugKGoRcCbaKjLQ3XZA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-7.2.0.tgz", + "integrity": "sha512-6639htZMjEkwskf3J+e6/iar+4cTNM9qhoWuRfj9F3eJD6r7iCzV1SWnQr2Mdv0QT0suuqU8BoJCZUyCtP9R4Q==", "dev": true, "license": "MIT", "dependencies": { - "@fortawesome/fontawesome-common-types": "7.1.0" + "@fortawesome/fontawesome-common-types": "7.2.0" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-brands-svg-icons": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-7.1.0.tgz", - "integrity": "sha512-9byUd9bgNfthsZAjBl6GxOu1VPHgBuRUP9juI7ZoM98h8xNPTCTagfwUFyYscdZq4Hr7gD1azMfM9s5tIWKZZA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-7.2.0.tgz", + "integrity": "sha512-VNG8xqOip1JuJcC3zsVsKRQ60oXG9+oYNDCosjoU/H9pgYmLTEwWw8pE0jhPz/JWdHeUuK6+NQ3qsM4gIbdbYQ==", "dev": true, "license": "(CC-BY-4.0 AND MIT)", "dependencies": { - "@fortawesome/fontawesome-common-types": "7.1.0" + "@fortawesome/fontawesome-common-types": "7.2.0" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-regular-svg-icons": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-7.1.0.tgz", - "integrity": "sha512-0e2fdEyB4AR+e6kU4yxwA/MonnYcw/CsMEP9lH82ORFi9svA6/RhDyhxIv5mlJaldmaHLLYVTb+3iEr+PDSZuQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-7.2.0.tgz", + "integrity": "sha512-iycmlN51EULlQ4D/UU9WZnHiN0CvjJ2TuuCrAh+1MVdzD+4ViKYH2deNAll4XAAYlZa8WAefHR5taSK8hYmSMw==", "dev": true, "license": "(CC-BY-4.0 AND MIT)", "dependencies": { - "@fortawesome/fontawesome-common-types": "7.1.0" + "@fortawesome/fontawesome-common-types": "7.2.0" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/free-solid-svg-icons": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.1.0.tgz", - "integrity": "sha512-Udu3K7SzAo9N013qt7qmm22/wo2hADdheXtBfxFTecp+ogsc0caQNRKEb7pkvvagUGOpG9wJC1ViH6WXs8oXIA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-7.2.0.tgz", + "integrity": "sha512-YTVITFGN0/24PxzXrwqCgnyd7njDuzp5ZvaCx5nq/jg55kUYd94Nj8UTchBdBofi/L0nwRfjGOg0E41d2u9T1w==", "dev": true, "license": "(CC-BY-4.0 AND MIT)", "dependencies": { - "@fortawesome/fontawesome-common-types": "7.1.0" + "@fortawesome/fontawesome-common-types": "7.2.0" }, "engines": { "node": ">=6" } }, "node_modules/@fortawesome/react-fontawesome": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-3.1.0.tgz", - "integrity": "sha512-5OUQH9aDH/xHJwnpD4J7oEdGvFGJgYnGe0UebaPIdMW9UxYC/f5jv2VjVEgnikdJN0HL8yQxp9Nq+7gqGZpIIA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-3.3.0.tgz", + "integrity": "sha512-EHmHeTf8WgO29sdY3iX/7ekE3gNUdlc2RW6mm/FzELlHFKfTrA9S4MlyquRR+RRCRCn8+jXfLFpLGB2l7wCWyw==", "dev": true, "license": "MIT", "engines": { @@ -2506,18 +2646,28 @@ "react": "^18.0.0 || ^19.0.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" }, "engines": { - "node": ">=10.10.0" + "node": ">=18.18.0" } }, "node_modules/@humanwhocodes/module-importer": { @@ -2533,11 +2683,19 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "dev": true + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@img/sharp-darwin-arm64": { "version": "0.33.5", @@ -2993,158 +3151,83 @@ "@types/node": ">=18" } }, - "node_modules/@isaacs/cliui": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", - "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^5.1.2", - "string-width-cjs": "npm:string-width@^4.2.0", - "strip-ansi": "^7.0.1", - "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", - "wrap-ansi": "^8.1.0", - "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@isaacs/cliui/node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true, - "license": "MIT" - }, - "node_modules/@isaacs/cliui/node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "node_modules/@jest/diff-sequences": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz", + "integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==", "dev": true, "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "node_modules/@jest/expect-utils": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz", + "integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "@jest/get-type": "30.1.0" }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", "dev": true, "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", "dev": true, "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/@jest/expect-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", - "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", - "dev": true, "dependencies": { - "jest-get-type": "^29.6.3" + "@types/node": "*", + "jest-regex-util": "30.0.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", "dev": true, + "license": "MIT", "dependencies": { - "@sinclair/typebox": "^0.27.8" + "@sinclair/typebox": "^0.34.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jridgewell/gen-mapping": { @@ -3158,6 +3241,17 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -3179,16 +3273,16 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -3197,9 +3291,9 @@ } }, "node_modules/@mantine/core": { - "version": "8.3.9", - "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.9.tgz", - "integrity": "sha512-ivj0Crn5N521cI2eWZBsBGckg0ZYRqfOJz5vbbvYmfj65bp0EdsyqZuOxXzIcn2aUScQhskfvzyhV5XIUv81PQ==", + "version": "8.3.18", + "resolved": "https://registry.npmjs.org/@mantine/core/-/core-8.3.18.tgz", + "integrity": "sha512-9tph1lTVogKPjTx02eUxDUOdXacPzK62UuSqb4TdGliI54/Xgxftq0Dfqu6XuhCxn9J5MDJaNiLDvL/1KRkYqA==", "license": "MIT", "dependencies": { "@floating-ui/react": "^0.27.16", @@ -3210,7 +3304,7 @@ "type-fest": "^4.41.0" }, "peerDependencies": { - "@mantine/hooks": "8.3.9", + "@mantine/hooks": "8.3.18", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } @@ -3228,24 +3322,24 @@ } }, "node_modules/@mantine/dropzone": { - "version": "8.3.9", - "resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-8.3.9.tgz", - "integrity": "sha512-RXFpmHQnANMxa3qBssQyI/YS+Xzpr6N/vPWTXrAzjNrJApbwShlIKRDHNw4mmydYT3Wq9y86uzXFPsrPoaJWLA==", + "version": "8.3.18", + "resolved": "https://registry.npmjs.org/@mantine/dropzone/-/dropzone-8.3.18.tgz", + "integrity": "sha512-GaYUUl/382R7hl1g6heTCZ5a6T5x6qYPg0oID6ik/J0j7e5+XMZyTH5ITpaqpsBQ09GKKsF5y3iNehpSby8Kew==", "license": "MIT", "dependencies": { - "react-dropzone": "14.3.8" + "react-dropzone": "15.0.0" }, "peerDependencies": { - "@mantine/core": "8.3.9", - "@mantine/hooks": "8.3.9", + "@mantine/core": "8.3.18", + "@mantine/hooks": "8.3.18", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "node_modules/@mantine/form": { - "version": "8.3.9", - "resolved": "https://registry.npmjs.org/@mantine/form/-/form-8.3.9.tgz", - "integrity": "sha512-G7eCo5TEWGcsWHrNGwp/o1yPcCYbaMdLq6SClN+wMhAA+qaEO1ZHf9mX8fSOX78nkaoyIrZHGOhbVXcZ7hmVrQ==", + "version": "8.3.18", + "resolved": "https://registry.npmjs.org/@mantine/form/-/form-8.3.18.tgz", + "integrity": "sha512-r5OGLJWTkmIruFjRZRZy9oA7maNYlyt50jB4Pmd2X5360WOmJLd4KH8MFhHZQC7vN+z8/rmBl3t3XGAR2I8xig==", "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.3", @@ -3256,55 +3350,55 @@ } }, "node_modules/@mantine/hooks": { - "version": "8.3.9", - "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.9.tgz", - "integrity": "sha512-Dfz7W0+K1cq4Gb1WFQCZn8tsMXkLH6MV409wZR/ToqsxdNDUMJ/xxbfnwEXWEZjXNJd1wDETHgc+cZG2lTe3Xw==", + "version": "8.3.18", + "resolved": "https://registry.npmjs.org/@mantine/hooks/-/hooks-8.3.18.tgz", + "integrity": "sha512-QoWr9+S8gg5050TQ06aTSxtlpGjYOpIllRbjYYXlRvZeTsUqiTbVfvQROLexu4rEaK+yy9Wwriwl9PMRgbLqPw==", "license": "MIT", "peerDependencies": { "react": "^18.x || ^19.x" } }, "node_modules/@mantine/modals": { - "version": "8.3.9", - "resolved": "https://registry.npmjs.org/@mantine/modals/-/modals-8.3.9.tgz", - "integrity": "sha512-0WOikHgECJeWA/1TNf+sxOnpNwQjmpyph3XEhzFkgneimW6Ry7R6qd/i345CDLSu6kP6FGGRI73SUROiTcu2Ng==", + "version": "8.3.18", + "resolved": "https://registry.npmjs.org/@mantine/modals/-/modals-8.3.18.tgz", + "integrity": "sha512-JfPDS4549L314SxFPC1x6CbKwzh82OdnIzwgMxPCVNsWLKV2vEHHUH/fzUYj4Wli6IBrsW4cufjMj9BTj3hm3Q==", "license": "MIT", "peerDependencies": { - "@mantine/core": "8.3.9", - "@mantine/hooks": "8.3.9", + "@mantine/core": "8.3.18", + "@mantine/hooks": "8.3.18", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "node_modules/@mantine/notifications": { - "version": "8.3.9", - "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-8.3.9.tgz", - "integrity": "sha512-emUdoCyaccf/NuNmJ4fQgloJ7hEod0Pde7XIoD9xUUztVchL143oWRU2gYm6cwqzSyjpjTaqPXfz5UvEBRYjZw==", + "version": "8.3.18", + "resolved": "https://registry.npmjs.org/@mantine/notifications/-/notifications-8.3.18.tgz", + "integrity": "sha512-IpQ0lmwbigTBbZCR6iSYWqIOKEx1tlcd7PcEJ5M5X1qeVSY/N3mmDQt1eJmObvcyDeL5cTJMbSA9UPqhRqo9jw==", "license": "MIT", "dependencies": { - "@mantine/store": "8.3.9", + "@mantine/store": "8.3.18", "react-transition-group": "4.4.5" }, "peerDependencies": { - "@mantine/core": "8.3.9", - "@mantine/hooks": "8.3.9", + "@mantine/core": "8.3.18", + "@mantine/hooks": "8.3.18", "react": "^18.x || ^19.x", "react-dom": "^18.x || ^19.x" } }, "node_modules/@mantine/store": { - "version": "8.3.9", - "resolved": "https://registry.npmjs.org/@mantine/store/-/store-8.3.9.tgz", - "integrity": "sha512-Z4tYW597mD3NxHLlJ3OJ1aKucmwrD9nhqobz+142JNw01aHqzKjxVXlu3L5GGa7F3u3OjXJk/qb1QmUs4sU+Jw==", + "version": "8.3.18", + "resolved": "https://registry.npmjs.org/@mantine/store/-/store-8.3.18.tgz", + "integrity": "sha512-i+QRTLmZzLldea0egtUVnGALd6UMIu8jd44nrNWBSNIXJU/8B6rMlC6gyX+l4szopZSuOaaNJIXkqRdC1gQsVg==", "license": "MIT", "peerDependencies": { "react": "^18.x || ^19.x" } }, "node_modules/@mswjs/interceptors": { - "version": "0.37.5", - "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.37.5.tgz", - "integrity": "sha512-AAwRb5vXFcY4L+FvZ7LZusDuZ0vEe0Zm8ohn1FM6/X7A3bj4mqmkAcGRWuvC2JwSygNwHAAmMnAI73vPHeqsHA==", + "version": "0.41.3", + "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.41.3.tgz", + "integrity": "sha512-cXu86tF4VQVfwz8W1SPbhoRyHJkti6mjH/XJIxp40jhO4j2k1m4KYrEykxqWPkFF3vrK4rgQppBh//AwyGSXPA==", "dev": true, "license": "MIT", "dependencies": { @@ -3319,41 +3413,6 @@ "node": ">=18" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/@open-draft/deferred-promise": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz", @@ -3379,99 +3438,463 @@ "dev": true, "license": "MIT" }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", "dev": true, + "hasInstallScript": true, "license": "MIT", "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, "engines": { - "node": ">=14" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "dev": true, - "license": "MIT" - }, - "node_modules/@quansync/fs": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-0.1.2.tgz", - "integrity": "sha512-ezIadUb1aFhwJLd++WVqVpi9rnlX8vnd4ju7saPhwLHJN1mJgOv0puePTGV+FbtSnWtwoHDT8lAm4kagDZmpCg==", + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "quansync": "^0.2.10" - }, + "optional": true, + "os": [ + "android" + ], "engines": { - "node": ">=20.0.0" + "node": ">= 10.0.0" }, "funding": { - "url": "https://github.com/sponsors/sxzz" + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-beta.32", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.32.tgz", - "integrity": "sha512-QReCdvxiUZAPkvp1xpAg62IeNzykOFA6syH2CnClif4YmALN1XKpB39XneL80008UbtMShthSVDKmrx05N1q/g==", - "dev": true, - "license": "MIT" - }, - "node_modules/@rollup/plugin-node-resolve": { - "version": "15.3.1", - "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", - "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "dependencies": { - "@rollup/pluginutils": "^5.0.1", - "@types/resolve": "1.20.2", - "deepmerge": "^4.2.2", - "is-module": "^1.0.0", - "resolve": "^1.22.1" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.78.0||^3.0.0||^4.0.0" + "node": ">= 10.0.0" }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rollup/plugin-terser": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", - "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", - "dependencies": { - "serialize-javascript": "^6.0.1", - "smob": "^1.0.0", - "terser": "^5.17.4" - }, + "optional": true, + "os": [ + "darwin" + ], "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "rollup": "^2.0.0||^3.0.0||^4.0.0" + "node": ">= 10.0.0" }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@rollup/pluginutils": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", - "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@polka/url": { + "version": "1.0.0-next.29", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", + "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", + "dev": true, + "license": "MIT" + }, + "node_modules/@quansync/fs": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-0.1.2.tgz", + "integrity": "sha512-ezIadUb1aFhwJLd++WVqVpi9rnlX8vnd4ju7saPhwLHJN1mJgOv0puePTGV+FbtSnWtwoHDT8lAm4kagDZmpCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quansync": "^0.2.10" + }, + "engines": { + "node": ">=20.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/@reduxjs/toolkit": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz", + "integrity": "sha512-Kd6kAHTA6/nUpp8mySPqj3en3dm0tdMIgbttnQ1xFMVpufoj+ADi8pXLBsd4xzTRHQa7t/Jv8W5UnCuW4kuWMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "@standard-schema/utils": "^0.3.0", + "immer": "^11.0.0", + "redux": "^5.0.1", + "redux-thunk": "^3.1.0", + "reselect": "^5.1.0" + }, + "peerDependencies": { + "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", + "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-redux": { + "optional": true + } + } + }, + "node_modules/@reduxjs/toolkit/node_modules/immer": { + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/immer/-/immer-11.1.4.tgz", + "integrity": "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/immer" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.3", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.3.tgz", + "integrity": "sha512-eybk3TjzzzV97Dlj5c+XrBFW57eTNhzod66y9HrBlzJ6NsCrWCp/2kaPS3K9wJmurBC0Tdw4yPjXKZqlznim3Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/plugin-terser": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz", + "integrity": "sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "serialize-javascript": "^6.0.1", + "smob": "^1.0.0", + "terser": "^5.17.4" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", "dev": true, "license": "MIT", "dependencies": { @@ -3499,9 +3922,9 @@ "license": "MIT" }, "node_modules/@rollup/pluginutils/node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", "engines": { @@ -3512,9 +3935,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.48.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.48.1.tgz", - "integrity": "sha512-rGmb8qoG/zdmKoYELCBwu7vt+9HxZ7Koos3pD0+sH5fR3u3Wb/jGcpnqxcnWsPEKDUyzeLSqksN8LJtgXjqBYw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", + "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", "cpu": [ "arm" ], @@ -3526,9 +3949,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.48.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.48.1.tgz", - "integrity": "sha512-4e9WtTxrk3gu1DFE+imNJr4WsL13nWbD/Y6wQcyku5qadlKHY3OQ3LJ/INrrjngv2BJIHnIzbqMk1GTAC2P8yQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", + "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", "cpu": [ "arm64" ], @@ -3540,9 +3963,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.48.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.48.1.tgz", - "integrity": "sha512-+XjmyChHfc4TSs6WUQGmVf7Hkg8ferMAE2aNYYWjiLzAS/T62uOsdfnqv+GHRjq7rKRnYh4mwWb4Hz7h/alp8A==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", + "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", "cpu": [ "arm64" ], @@ -3554,9 +3977,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.48.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.48.1.tgz", - "integrity": "sha512-upGEY7Ftw8M6BAJyGwnwMw91rSqXTcOKZnnveKrVWsMTF8/k5mleKSuh7D4v4IV1pLxKAk3Tbs0Lo9qYmii5mQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", + "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", "cpu": [ "x64" ], @@ -3568,9 +3991,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.48.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.48.1.tgz", - "integrity": "sha512-P9ViWakdoynYFUOZhqq97vBrhuvRLAbN/p2tAVJvhLb8SvN7rbBnJQcBu8e/rQts42pXGLVhfsAP0k9KXWa3nQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", + "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", "cpu": [ "arm64" ], @@ -3582,9 +4005,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.48.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.48.1.tgz", - "integrity": "sha512-VLKIwIpnBya5/saccM8JshpbxfyJt0Dsli0PjXozHwbSVaHTvWXJH1bbCwPXxnMzU4zVEfgD1HpW3VQHomi2AQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", + "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", "cpu": [ "x64" ], @@ -3596,9 +4019,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.48.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.48.1.tgz", - "integrity": "sha512-3zEuZsXfKaw8n/yF7t8N6NNdhyFw3s8xJTqjbTDXlipwrEHo4GtIKcMJr5Ed29leLpB9AugtAQpAHW0jvtKKaQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", + "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", "cpu": [ "arm" ], @@ -3610,9 +4033,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.48.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.48.1.tgz", - "integrity": "sha512-leo9tOIlKrcBmmEypzunV/2w946JeLbTdDlwEZ7OnnsUyelZ72NMnT4B2vsikSgwQifjnJUbdXzuW4ToN1wV+Q==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", + "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", "cpu": [ "arm" ], @@ -3624,9 +4047,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.48.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.48.1.tgz", - "integrity": "sha512-Vy/WS4z4jEyvnJm+CnPfExIv5sSKqZrUr98h03hpAMbE2aI0aD2wvK6GiSe8Gx2wGp3eD81cYDpLLBqNb2ydwQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", + "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", "cpu": [ "arm64" ], @@ -3638,9 +4061,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.48.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.48.1.tgz", - "integrity": "sha512-x5Kzn7XTwIssU9UYqWDB9VpLpfHYuXw5c6bJr4Mzv9kIv242vmJHbI5PJJEnmBYitUIfoMCODDhR7KoZLot2VQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", + "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", "cpu": [ "arm64" ], @@ -3651,10 +4074,24 @@ "linux" ] }, - "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.48.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.48.1.tgz", - "integrity": "sha512-yzCaBbwkkWt/EcgJOKDUdUpMHjhiZT/eDktOPWvSRpqrVE04p0Nd6EGV4/g7MARXXeOqstflqsKuXVM3H9wOIQ==", + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", + "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", + "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", "cpu": [ "loong64" ], @@ -3666,9 +4103,23 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.48.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.48.1.tgz", - "integrity": "sha512-UK0WzWUjMAJccHIeOpPhPcKBqax7QFg47hwZTp6kiMhQHeOYJeaMwzeRZe1q5IiTKsaLnHu9s6toSYVUlZ2QtQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", + "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", + "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", "cpu": [ "ppc64" ], @@ -3680,9 +4131,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.48.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.48.1.tgz", - "integrity": "sha512-3NADEIlt+aCdCbWVZ7D3tBjBX1lHpXxcvrLt/kdXTiBrOds8APTdtk2yRL2GgmnSVeX4YS1JIf0imFujg78vpw==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", + "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", "cpu": [ "riscv64" ], @@ -3694,9 +4145,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.48.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.48.1.tgz", - "integrity": "sha512-euuwm/QTXAMOcyiFCcrx0/S2jGvFlKJ2Iro8rsmYL53dlblp3LkUQVFzEidHhvIPPvcIsxDhl2wkBE+I6YVGzA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", + "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", "cpu": [ "riscv64" ], @@ -3708,9 +4159,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.48.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.48.1.tgz", - "integrity": "sha512-w8mULUjmPdWLJgmTYJx/W6Qhln1a+yqvgwmGXcQl2vFBkWsKGUBRbtLRuKJUln8Uaimf07zgJNxOhHOvjSQmBQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", + "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", "cpu": [ "s390x" ], @@ -3722,9 +4173,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.48.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.48.1.tgz", - "integrity": "sha512-90taWXCWxTbClWuMZD0DKYohY1EovA+W5iytpE89oUPmT5O1HFdf8cuuVIylE6vCbrGdIGv85lVRzTcpTRZ+kA==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", + "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", "cpu": [ "x64" ], @@ -3736,9 +4187,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.48.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.48.1.tgz", - "integrity": "sha512-2Gu29SkFh1FfTRuN1GR1afMuND2GKzlORQUP3mNMJbqdndOg7gNsa81JnORctazHRokiDzQ5+MLE5XYmZW5VWg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", + "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", "cpu": [ "x64" ], @@ -3749,10 +4200,38 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", + "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", + "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.48.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.48.1.tgz", - "integrity": "sha512-6kQFR1WuAO50bxkIlAVeIYsz3RUx+xymwhTo9j94dJ+kmHe9ly7muH23sdfWduD0BA8pD9/yhonUvAjxGh34jQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", + "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", "cpu": [ "arm64" ], @@ -3764,9 +4243,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.48.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.48.1.tgz", - "integrity": "sha512-RUyZZ/mga88lMI3RlXFs4WQ7n3VyU07sPXmMG7/C1NOi8qisUg57Y7LRarqoGoAiopmGmChUhSwfpvQ3H5iGSQ==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", + "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", "cpu": [ "ia32" ], @@ -3777,10 +4256,24 @@ "win32" ] }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", + "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.48.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.48.1.tgz", - "integrity": "sha512-8a/caCUN4vkTChxkaIJcMtwIVcBhi4X2PQRoT+yCK3qRYaZ7cURrmJFL5Ux9H9RaMIXj9RuihckdmkBX3zZsgg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", + "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", "cpu": [ "x64" ], @@ -3792,16 +4285,31 @@ ] }, "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true, + "license": "MIT" }, "node_modules/@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", + "dev": true, + "license": "MIT" + }, "node_modules/@surma/rollup-plugin-off-main-thread": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz", @@ -3826,9 +4334,9 @@ } }, "node_modules/@tanstack/query-core": { - "version": "5.64.1", - "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.64.1.tgz", - "integrity": "sha512-978Wx4Wl4UJZbmvU/rkaM9cQtXXrbhK0lsz/UZhYIbyKYA8E4LdomTwyh2GHZ4oU0BKKoDH4YlKk2VscCUgNmg==", + "version": "5.95.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.95.2.tgz", + "integrity": "sha512-o4T8vZHZET4Bib3jZ/tCW9/7080urD4c+0/AUaYVpIqOsr7y0reBc1oX3ttNaSW5mYyvZHctiQ/UOP2PfdmFEQ==", "license": "MIT", "funding": { "type": "github", @@ -3836,9 +4344,9 @@ } }, "node_modules/@tanstack/query-devtools": { - "version": "5.37.1", - "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.37.1.tgz", - "integrity": "sha512-XcG4IIHIv0YQKrexTqo2zogQWR1Sz672tX2KsfE9kzB+9zhx44vRKH5si4WDILE1PIWQpStFs/NnrDQrBAUQpg==", + "version": "5.95.2", + "resolved": "https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.95.2.tgz", + "integrity": "sha512-QfaoqBn9uAZ+ICkA8brd1EHj+qBF6glCFgt94U8XP5BT6ppSsDBI8IJ00BU+cAGjQzp6wcKJL2EmRYvxy0TWIg==", "dev": true, "license": "MIT", "funding": { @@ -3847,12 +4355,12 @@ } }, "node_modules/@tanstack/react-query": { - "version": "5.64.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.64.1.tgz", - "integrity": "sha512-vW5ggHpIO2Yjj44b4sB+Fd3cdnlMJppXRBJkEHvld6FXh3j5dwWJoQo7mGtKI2RbSFyiyu/PhGAy0+Vv5ev9Eg==", + "version": "5.95.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.95.2.tgz", + "integrity": "sha512-/wGkvLj/st5Ud1Q76KF1uFxScV7WeqN1slQx5280ycwAyYkIPGaRZAEgHxe3bjirSd5Zpwkj6zNcR4cqYni/ZA==", "license": "MIT", "dependencies": { - "@tanstack/query-core": "5.64.1" + "@tanstack/query-core": "5.95.2" }, "funding": { "type": "github", @@ -3863,30 +4371,30 @@ } }, "node_modules/@tanstack/react-query-devtools": { - "version": "5.40.1", - "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.40.1.tgz", - "integrity": "sha512-/AN2UsbuL+28/KSlBkVHq/4chHTEp4l2UWTKWixXbn4pprLQrZGmQTAKN4tYxZDuNwNZY5+Zp67pDfXj+F/UBA==", + "version": "5.95.2", + "resolved": "https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.95.2.tgz", + "integrity": "sha512-AFQFmbznVkbtfpx8VJ2DylW17wWagQel/qLstVLkYmNRo2CmJt3SNej5hvl6EnEeljJIdC3BTB+W7HZtpsH+3g==", "dev": true, "license": "MIT", "dependencies": { - "@tanstack/query-devtools": "5.37.1" + "@tanstack/query-devtools": "5.95.2" }, "funding": { "type": "github", "url": "https://github.com/sponsors/tannerlinsley" }, "peerDependencies": { - "@tanstack/react-query": "^5.40.1", + "@tanstack/react-query": "^5.95.2", "react": "^18 || ^19" } }, "node_modules/@tanstack/react-table": { - "version": "8.19.2", - "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.19.2.tgz", - "integrity": "sha512-itoSIAkA/Vsg+bjY23FSemcTyPhc5/1YjYyaMsr9QSH/cdbZnQxHVWrpWn0Sp2BWN71qkzR7e5ye8WuMmwyOjg==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", + "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", "license": "MIT", "dependencies": { - "@tanstack/table-core": "8.19.2" + "@tanstack/table-core": "8.21.3" }, "engines": { "node": ">=12" @@ -3900,10 +4408,27 @@ "react-dom": ">=16.8" } }, + "node_modules/@tanstack/react-virtual": { + "version": "3.13.23", + "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.23.tgz", + "integrity": "sha512-XnMRnHQ23piOVj2bzJqHrRrLg4r+F86fuBcwteKfbIjJrtGxb4z7tIvPVAe4B+4UVwo9G4Giuz5fmapcrnZ0OQ==", + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.23" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/@tanstack/table-core": { - "version": "8.19.2", - "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.19.2.tgz", - "integrity": "sha512-KpRjhgehIhbfH78ARm/GJDXGnpdw4bCg3qas6yjWSi7czJhI/J6pWln7NHtmBkGE9ZbohiiNtLqwGzKmBfixig==", + "version": "8.21.3", + "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", + "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", "license": "MIT", "engines": { "node": ">=12" @@ -3913,20 +4438,30 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.23", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.23.tgz", + "integrity": "sha512-zSz2Z2HNyLjCplANTDyl3BcdQJc2k1+yyFoKhNRmCr7V7dY8o8q5m8uFTI1/Pg1kL+Hgrz6u3Xo6eFUB7l66cg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, "node_modules/@testing-library/dom": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.0.0.tgz", - "integrity": "sha512-PmJPnogldqoVFf+EwbHvbBJ98MmqASV8kLrBYgsDNxQcFMeIS7JFL48sfyXvuMtgmWO/wMhh25odr+8VhDmn4g==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", "dev": true, - "peer": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", - "chalk": "^4.1.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", + "picocolors": "1.1.1", "pretty-format": "^27.0.2" }, "engines": { @@ -3934,61 +4469,23 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.2.tgz", - "integrity": "sha512-CzqH0AFymEMG48CpzXFriYYkOjk6ZGPCLMhW9e9jg3KMCn5OfJecF8GtGW7yGfR/IgCe3SX8BSwjdzI6BBbZLw==", + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", "dev": true, + "license": "MIT", "dependencies": { - "@adobe/css-tools": "^4.3.2", - "@babel/runtime": "^7.9.2", + "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", - "chalk": "^3.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", - "lodash": "^4.17.15", + "picocolors": "^1.1.1", "redent": "^3.0.0" }, "engines": { "node": ">=14", "npm": ">=6", "yarn": ">=1" - }, - "peerDependencies": { - "@jest/globals": ">= 28", - "@types/bun": "latest", - "@types/jest": ">= 28", - "jest": ">= 28", - "vitest": ">= 0.32" - }, - "peerDependenciesMeta": { - "@jest/globals": { - "optional": true - }, - "@types/bun": { - "optional": true - }, - "@types/jest": { - "optional": true - }, - "jest": { - "optional": true - }, - "vitest": { - "optional": true - } - } - }, - "node_modules/@testing-library/jest-dom/node_modules/chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=8" } }, "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { @@ -3998,9 +4495,9 @@ "dev": true }, "node_modules/@testing-library/react": { - "version": "16.1.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.1.0.tgz", - "integrity": "sha512-Q2ToPvg0KsVL0ohND9A3zLJWcOXXcO8IDu3fj11KhNt0UlCWyFyvnCIBkd12tidB2lkiVRG8VFqdhcqhqnAQtg==", + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", "dev": true, "license": "MIT", "dependencies": { @@ -4026,10 +4523,11 @@ } }, "node_modules/@testing-library/user-event": { - "version": "14.5.2", - "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.5.2.tgz", - "integrity": "sha512-YAh82Wh4TIrxYLmfGcixwD18oIjyC1pFQC2Y01F2lzV2HTMiYrI0nze0FD0ocB//CKS/7jIUgae+adPqxK5yCQ==", + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", "dev": true, + "license": "MIT", "engines": { "node": ">=12", "npm": ">=6" @@ -4042,8 +4540,7 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true, - "peer": true + "dev": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -4087,84 +4584,87 @@ } }, "node_modules/@types/chai": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", - "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, "license": "MIT", "dependencies": { - "@types/deep-eql": "*" + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" } }, - "node_modules/@types/cookie": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.6.0.tgz", - "integrity": "sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==", + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", "dev": true, "license": "MIT" }, - "node_modules/@types/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", - "dev": true - }, "node_modules/@types/d3-color": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/d3-ease": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/d3-interpolate": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", "dev": true, + "license": "MIT", "dependencies": { "@types/d3-color": "*" } }, "node_modules/@types/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==", - "dev": true + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "dev": true, + "license": "MIT" }, "node_modules/@types/d3-scale": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.8.tgz", - "integrity": "sha512-gkK1VVTr5iNiYJ7vWDI+yUFFlszhNMtVeneJ6lUTKPjprsvLLI9/tgEGiXJOnlINJA8FyA88gfnQsHbybVZrYQ==", + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", "dev": true, + "license": "MIT", "dependencies": { "@types/d3-time": "*" } }, "node_modules/@types/d3-shape": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.6.tgz", - "integrity": "sha512-5KKk5aKGu2I+O6SONMYSNflgiP0WfZIQvVUMan50wHsLG1G94JlxEVnCpQARfTtzytuY0p/9PXXZb3I7giofIA==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", "dev": true, + "license": "MIT", "dependencies": { "@types/d3-path": "*" } }, "node_modules/@types/d3-time": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.3.tgz", - "integrity": "sha512-2p6olUZ4w3s+07q3Tm2dbiMZy5pCDfYwtLXXHUnVzXgQlZ/OyPtUz6OL382BkOuGlLXqfT+wqv8Fw2v8/0geBw==", - "dev": true + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "dev": true, + "license": "MIT" }, "node_modules/@types/d3-timer": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/deep-eql": { "version": "4.0.2", @@ -4173,6 +4673,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -4184,13 +4691,15 @@ "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/istanbul-lib-report": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", "dev": true, + "license": "MIT", "dependencies": { "@types/istanbul-lib-coverage": "*" } @@ -4200,18 +4709,20 @@ "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/istanbul-lib-report": "*" } }, "node_modules/@types/jest": { - "version": "29.5.12", - "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.12.tgz", - "integrity": "sha512-eDC8bTvT/QhYdxJAulQikueigY5AsdBRH2yDKW3yveW7svY3+DzN84/2NUgkw10RTiJbWqZrTtoGVdYlvFJdLw==", + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", "dev": true, + "license": "MIT", "dependencies": { - "expect": "^29.0.0", - "pretty-format": "^29.0.0" + "expect": "^30.0.0", + "pretty-format": "^30.0.0" } }, "node_modules/@types/jest/node_modules/ansi-styles": { @@ -4219,6 +4730,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -4227,52 +4739,56 @@ } }, "node_modules/@types/jest/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@types/jest/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/lodash": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.1.tgz", - "integrity": "sha512-X+2qazGS3jxLAIz5JDXDzglAF3KpijdhFxlf/V1+hEsOUc+HnWi81L/uv/EvGuV90WY+7mPGFCUDGfQC3Gj95Q==", - "dev": true + "version": "4.17.24", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz", + "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==", + "dev": true, + "license": "MIT" }, "node_modules/@types/node": { - "version": "22.14.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz", - "integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==", + "version": "25.5.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.0.tgz", + "integrity": "sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~6.21.0" + "undici-types": "~7.18.0" } }, "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==", - "devOptional": true, + "version": "19.2.14", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", + "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", + "dev": true, "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -4295,29 +4811,17 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/semver": { - "version": "7.5.8", - "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", - "integrity": "sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==", - "dev": true - }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", - "dev": true - }, - "node_modules/@types/statuses": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz", - "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==", "dev": true, "license": "MIT" }, - "node_modules/@types/tough-cookie": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", - "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "node_modules/@types/statuses": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz", + "integrity": "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==", "dev": true, "license": "MIT" }, @@ -4328,11 +4832,19 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/use-sync-external-store": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", + "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/yargs": { - "version": "17.0.32", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.32.tgz", - "integrity": "sha512-xQ67Yc/laOG5uMfX/093MRlGGCIBzZMarVa+gfNKJxWAIgykYpVGkBdbqEzGDDfCrVUj6Hiff4mTZ5BA6TmAog==", + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", "dev": true, + "license": "MIT", "dependencies": { "@types/yargs-parser": "*" } @@ -4341,523 +4853,238 @@ "version": "21.0.3", "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz", - "integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.2.tgz", + "integrity": "sha512-NZZgp0Fm2IkD+La5PR81sd+g+8oS6JwJje+aRWsDocxHkjyRw0J5L5ZTlN3LI1LlOcGL7ph3eaIUmTXMIjLk0w==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/type-utils": "7.16.0", - "@typescript-eslint/utils": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", - "graphemer": "^1.4.0", - "ignore": "^5.3.1", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/type-utils": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^7.0.0", - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz", - "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz", - "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz", - "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz", - "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/typescript-estree": "7.16.0" + "ts-api-utils": "^2.4.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz", - "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.16.0", - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "@typescript-eslint/parser": "^8.57.2", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, "engines": { - "node": ">=10" + "node": ">= 4" } }, "node_modules/@typescript-eslint/parser": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz", - "integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.2.tgz", + "integrity": "sha512-30ScMRHIAD33JJQkgfGW1t8CURZtjc2JpTrq5n2HFhOefbAhb7ucc7xJwdWcrEtqUIYJ73Nybpsggii6GtAHjA==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/typescript-estree": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz", - "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz", - "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==", + "node_modules/@typescript-eslint/parser/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz", - "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==", - "dev": true, - "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "ms": "^2.1.3" }, "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=6.0" }, "peerDependenciesMeta": { - "typescript": { + "supports-color": { "optional": true } } }, - "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz", - "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "7.16.0", - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@typescript-eslint/parser/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", - "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", - "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/@typescript-eslint/type-utils": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz", - "integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/typescript-estree": "7.16.0", - "@typescript-eslint/utils": "7.16.0", - "debug": "^4.3.4", - "ts-api-utils": "^1.3.0" - }, - "engines": { - "node": "^18.18.0 || >=20.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.56.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } + "node_modules/@typescript-eslint/parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz", - "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==", + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.2.tgz", + "integrity": "sha512-FuH0wipFywXRTHf+bTTjNyuNQQsQC3qh/dYzaM4I4W0jrCqjCVuUh99+xd9KamUfmCGPvbO8NDngo/vsnNVqgw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0" + "@typescript-eslint/tsconfig-utils": "^8.57.2", + "@typescript-eslint/types": "^8.57.2", + "debug": "^4.4.3" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz", - "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==", + "node_modules/@typescript-eslint/project-service/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": ">=6.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz", - "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==", + "node_modules/@typescript-eslint/project-service/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, - "license": "BSD-2-Clause", + "license": "MIT" + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.2.tgz", + "integrity": "sha512-snZKH+W4WbWkrBqj4gUNRIGb/jipDW3qMqVJ4C9rzdFc+wLwruxk+2a5D+uoFcKPAqyqEnSb4l2ULuZf95eSkw==", + "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "minimatch": "^9.0.4", - "semver": "^7.6.0", - "ts-api-utils": "^1.3.0" + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz", - "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==", + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.2.tgz", + "integrity": "sha512-3Lm5DSM+DCowsUOJC+YqHHnKEfFh5CoGkj5Z31NQSNF4l5wdOwqGn99wmwN/LImhfY3KJnmordBq/4+VDe2eKw==", "dev": true, "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/typescript-estree": "7.16.0" - }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.56.0" + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz", - "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==", + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.2.tgz", + "integrity": "sha512-Co6ZCShm6kIbAM/s+oYVpKFfW7LBc6FXoPXjTRQ449PPNBY8U0KZXuevz5IFuuUj2H9ss40atTaf9dlGLzbWZg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.0", - "eslint-visitor-keys": "^3.4.3" + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" }, "engines": { - "node": "^18.18.0 || >=20.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/type-utils/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "node_modules/@typescript-eslint/type-utils/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/@typescript-eslint/type-utils/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" + "ms": "^2.1.3" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=6.0" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, - "node_modules/@typescript-eslint/type-utils/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "node_modules/@typescript-eslint/type-utils/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } + "license": "MIT" }, "node_modules/@typescript-eslint/types": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", - "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.2.tgz", + "integrity": "sha512-/iZM6FnM4tnx9csuTxspMW4BOSegshwX5oBDznJ7S4WggL7Vczz5d2W11ecc4vRrQMQHXRSxzrCsyG5EsPPTbA==", "dev": true, + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", @@ -4865,52 +5092,64 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", - "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.2.tgz", + "integrity": "sha512-2MKM+I6g8tJxfSmFKOnHv2t8Sk3T6rF20A1Puk0svLK+uVapDZB/4pfAeB7nE83uAZrU6OxW+HmOd5wHVdXwXA==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/visitor-keys": "5.62.0", - "debug": "^4.3.4", - "globby": "^11.1.0", - "is-glob": "^4.0.3", - "semver": "^7.3.7", - "tsutils": "^3.21.0" + "@typescript-eslint/project-service": "8.57.2", + "@typescript-eslint/tsconfig-utils": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/visitor-keys": "8.57.2", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, + "license": "MIT", "dependencies": { - "yallist": "^4.0.0" + "ms": "^2.1.3" }, "engines": { - "node": ">=10" + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -4918,120 +5157,65 @@ "node": ">=10" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/@typescript-eslint/utils": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", - "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.2.tgz", + "integrity": "sha512-krRIbvPK1ju1WBKIefiX+bngPs+odIQUtR7kymzPfo1POVw3jlF+nLkmexdSSd4UCbDcQn+wMBATOOmpBbqgKg==", "dev": true, + "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@types/json-schema": "^7.0.9", - "@types/semver": "^7.3.12", - "@typescript-eslint/scope-manager": "5.62.0", - "@typescript-eslint/types": "5.62.0", - "@typescript-eslint/typescript-estree": "5.62.0", - "eslint-scope": "^5.1.1", - "semver": "^7.3.7" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.2", + "@typescript-eslint/types": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "engines": { - "node": ">=4.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/utils/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.2.tgz", + "integrity": "sha512-zhahknjobV2FiD6Ee9iLbS7OV9zi10rG26odsQdfBO/hjSzUQbkIYgda+iNKK1zNiW2ey+Lf8MU5btN17V3dUw==", "dev": true, + "license": "MIT", "dependencies": { - "yallist": "^4.0.0" + "@typescript-eslint/types": "8.57.2", + "eslint-visitor-keys": "^5.0.0" }, "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, - "engines": { - "node": ">=10" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@typescript-eslint/utils/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "5.62.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", - "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, - "dependencies": { - "@typescript-eslint/types": "5.62.0", - "eslint-visitor-keys": "^3.3.0" - }, + "license": "Apache-2.0", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "url": "https://opencollective.com/eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, "node_modules/@vite-pwa/assets-generator": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@vite-pwa/assets-generator/-/assets-generator-1.0.0.tgz", - "integrity": "sha512-tWRF/tsqGkND5+dDVnJz7DzQkIRjtTRRYvA3y6l4FwTwK47OK72p1X7ResSz6T7PimIZMuFd+arsB8NRIG+Sww==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@vite-pwa/assets-generator/-/assets-generator-1.0.2.tgz", + "integrity": "sha512-MCbrb508JZHqe7bUibmZj/lyojdhLRnfkmyXnkrCM2zVrjTgL89U8UEfInpKTvPeTnxsw2hmyZxnhsdNR6yhwg==", "dev": true, "license": "MIT", "dependencies": { @@ -5053,53 +5237,81 @@ } }, "node_modules/@vitejs/plugin-react": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.0.1.tgz", - "integrity": "sha512-DE4UNaBXwtVoDJ0ccBdLVjFTWL70NRuWNCxEieTI3lrq9ORB9aOCQEKstwDXBl87NvFdbqh/p7eINGyj0BthJA==", + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", + "integrity": "sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.28.3", + "@babel/core": "^7.29.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", - "@rolldown/pluginutils": "1.0.0-beta.32", + "@rolldown/pluginutils": "1.0.0-rc.3", "@types/babel__core": "^7.20.5", - "react-refresh": "^0.17.0" + "react-refresh": "^0.18.0" }, "engines": { "node": "^20.19.0 || >=22.12.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/@vitejs/plugin-react/node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, "node_modules/@vitest/coverage-v8": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", - "integrity": "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.2.tgz", + "integrity": "sha512-sPK//PHO+kAkScb8XITeB1bf7fsk85Km7+rt4eeuRR3VS1/crD47cmV5wicisJmjNdfeokTZwjMk4Mj2d58Mgg==", "dev": true, "license": "MIT", "dependencies": { - "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", - "ast-v8-to-istanbul": "^0.3.3", - "debug": "^4.4.1", + "@vitest/utils": "4.1.2", + "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.6", - "istanbul-reports": "^3.1.7", - "magic-string": "^0.30.17", - "magicast": "^0.3.5", - "std-env": "^3.9.0", - "test-exclude": "^7.0.1", - "tinyrainbow": "^2.0.0" + "istanbul-reports": "^3.2.0", + "magicast": "^0.5.2", + "obug": "^2.1.1", + "std-env": "^4.0.0-rc.1", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "3.2.4", - "vitest": "3.2.4" + "@vitest/browser": "4.1.2", + "vitest": "4.1.2" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -5107,65 +5319,41 @@ } } }, - "node_modules/@vitest/coverage-v8/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@vitest/coverage-v8/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", + "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", "dev": true, "license": "MIT", "dependencies": { + "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "tinyrainbow": "^2.0.0" + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", + "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.4", + "@vitest/spy": "4.1.2", "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" + "magic-string": "^0.30.21" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -5177,42 +5365,42 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", + "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^2.0.0" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", + "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.4", - "pathe": "^2.0.3", - "strip-literal": "^3.0.0" + "@vitest/utils": "4.1.2", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", + "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", - "magic-string": "^0.30.17", + "@vitest/pretty-format": "4.1.2", + "@vitest/utils": "4.1.2", + "magic-string": "^0.30.21", "pathe": "^2.0.3" }, "funding": { @@ -5220,60 +5408,58 @@ } }, "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", + "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^4.0.3" - }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/ui": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-3.2.4.tgz", - "integrity": "sha512-hGISOaP18plkzbWEcP/QvtRW1xDXF2+96HbEX6byqQhAUbiS5oH6/9JwW+QsQCIYON2bI6QZBF+2PvOmrRZ9wA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/ui/-/ui-4.1.2.tgz", + "integrity": "sha512-/irhyeAcKS2u6Zokagf9tqZJ0t8S6kMZq4ZG9BHZv7I+fkRrYfQX4w7geYeC2r6obThz39PDxvXQzZX+qXqGeg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.4", + "@vitest/utils": "4.1.2", "fflate": "^0.8.2", - "flatted": "^3.3.3", + "flatted": "^3.4.2", "pathe": "^2.0.3", - "sirv": "^3.0.1", - "tinyglobby": "^0.2.14", - "tinyrainbow": "^2.0.0" + "sirv": "^3.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "3.2.4" + "vitest": "4.1.2" } }, "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", + "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", - "loupe": "^3.1.4", - "tinyrainbow": "^2.0.0" + "@vitest/pretty-format": "4.1.2", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/acorn": { - "version": "8.11.3", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", - "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -5286,6 +5472,7 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -5301,10 +5488,11 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, + "license": "MIT", "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -5355,12 +5543,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true - }, "node_modules/aria-query": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", @@ -5387,15 +5569,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/arraybuffer.prototype.slice": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", @@ -5429,21 +5602,21 @@ } }, "node_modules/ast-v8-to-istanbul": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.4.tgz", - "integrity": "sha512-cxrAnZNLBnQwBPByK4CeDaw5sWZtMilJE/Q3iDA0aamgaIVNDF9T6K2/8DfYDZEejZ2jNnDrG9m8MY72HFd0KA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-1.0.0.tgz", + "integrity": "sha512-1fSfIwuDICFA4LKkCzRPO7F0hzFf0B7+Xqrl27ynQaa+Rh0e1Es0v6kWHPott3lU10AyAr7oKHa65OppjLn3Rg==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.29", + "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", - "js-tokens": "^9.0.1" + "js-tokens": "^10.0.0" } }, "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", "dev": true, "license": "MIT" }, @@ -5467,7 +5640,8 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" }, "node_modules/at-least-node": { "version": "1.0.0", @@ -5505,25 +5679,25 @@ } }, "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", + "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", "license": "MIT", "dependencies": { - "follow-redirects": "^1.15.6", - "form-data": "^4.0.4", - "proxy-from-env": "^1.1.0" + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.13", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz", - "integrity": "sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g==", + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.16.tgz", + "integrity": "sha512-xaVwwSfebXf0ooE11BJovZYKhFjIvQo7TsyVpETuIeH2JHv0k/T6Y5j22pPTvqYqmpkxdlPAJlyJ0tfOJAoMxw==", "dev": true, "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.4", + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.7", "semver": "^6.3.1" }, "peerDependencies": { @@ -5531,47 +5705,82 @@ } }, "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", - "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.14.1.tgz", + "integrity": "sha512-ENp89vM9Pw4kv/koBb5N2f9bDZsR0hpf3BdPMOg/pkS3pwO4dzNnQZVXtBbeyAadgm865DmQG2jMMLqmZXvuCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.7", + "core-js-compat": "^3.48.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.7", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.7.tgz", + "integrity": "sha512-OTYbUlSwXhNgr4g6efMZgsO8//jA61P7ZbRX3iTT53VON8l+WQS8IAUEVo4a4cWknrg2W8Cj4gQhRYNCJ8GkAA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.3", - "core-js-compat": "^3.40.0" + "@babel/helper-define-polyfill-provider": "^0.6.7" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz", - "integrity": "sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw==", + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.4" + "balanced-match": "^4.0.2" }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + "engines": { + "node": "18 || 20 || >=22" } }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "node_modules/brace-expansion/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { @@ -5586,9 +5795,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, "funding": [ { @@ -5606,10 +5815,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -5618,13 +5828,6 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "node_modules/buffer-builder": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz", - "integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==", - "dev": true, - "license": "MIT/X11" - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -5690,15 +5893,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -5731,18 +5925,11 @@ "license": "CC-BY-4.0" }, "node_modules/chai": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", - "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "dev": true, "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, "engines": { "node": ">=18" } @@ -5752,6 +5939,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -5763,20 +5951,26 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/check-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, "engines": { - "node": ">= 16" + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" } }, "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", "dev": true, "funding": [ { @@ -5784,6 +5978,7 @@ "url": "https://github.com/sponsors/sibiraj-s" } ], + "license": "MIT", "engines": { "node": ">=8" } @@ -5901,6 +6096,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" }, @@ -5918,12 +6114,6 @@ "node": ">=4.0.0" } }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true - }, "node_modules/consola": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", @@ -5934,6 +6124,13 @@ "node": "^14.18.0 || >=16.10.0" } }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cookie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.0.2.tgz", @@ -5944,13 +6141,13 @@ } }, "node_modules/core-js-compat": { - "version": "3.41.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz", - "integrity": "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A==", + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz", + "integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==", "dev": true, "license": "MIT", "dependencies": { - "browserslist": "^4.24.4" + "browserslist": "^4.28.1" }, "funding": { "type": "opencollective", @@ -5982,6 +6179,20 @@ "node": ">=8" } }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -6002,17 +6213,29 @@ } }, "node_modules/cssstyle": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.2.1.tgz", - "integrity": "sha512-9+vem03dMXG7gDmZ62uqmRiMRNtinIZ9ZyuF6BdxzfOD+FdN5hretzynkn0ReS2DO2GSw76RWHs0UmJPI2zUjw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-6.2.0.tgz", + "integrity": "sha512-Fm5NvhYathRnXNVndkUsCCuR63DCLVVwGOOwQw782coXFi5HhkXdu289l59HlXZBawsyNccXfWRYvLzcDCdDig==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^2.8.2", - "rrweb-cssom": "^0.8.0" + "@asamuzakjp/css-color": "^5.0.1", + "@csstools/css-syntax-patches-for-csstree": "^1.0.28", + "css-tree": "^3.1.0", + "lru-cache": "^11.2.6" }, "engines": { - "node": ">=18" + "node": ">=20" + } + }, + "node_modules/cssstyle/node_modules/lru-cache": { + "version": "11.2.7", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", + "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" } }, "node_modules/csstype": { @@ -6026,6 +6249,7 @@ "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", "dev": true, + "license": "ISC", "dependencies": { "internmap": "1 - 2" }, @@ -6038,6 +6262,7 @@ "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } @@ -6047,15 +6272,17 @@ "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=12" } }, "node_modules/d3-format": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", - "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } @@ -6065,6 +6292,7 @@ "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", "dev": true, + "license": "ISC", "dependencies": { "d3-color": "1 - 3" }, @@ -6077,6 +6305,7 @@ "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } @@ -6086,6 +6315,7 @@ "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", "dev": true, + "license": "ISC", "dependencies": { "d3-array": "2.10.0 - 3", "d3-format": "1 - 3", @@ -6102,6 +6332,7 @@ "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", "dev": true, + "license": "ISC", "dependencies": { "d3-path": "^3.1.0" }, @@ -6114,6 +6345,7 @@ "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", "dev": true, + "license": "ISC", "dependencies": { "d3-array": "2 - 3" }, @@ -6126,6 +6358,7 @@ "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", "dev": true, + "license": "ISC", "dependencies": { "d3-time": "1 - 3" }, @@ -6138,21 +6371,23 @@ "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } }, "node_modules/data-urls": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", - "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", "dev": true, + "license": "MIT", "dependencies": { - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.0.0" + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/data-view-buffer": { @@ -6226,16 +6461,18 @@ } }, "node_modules/decimal.js": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz", - "integrity": "sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA==", - "dev": true + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" }, "node_modules/decimal.js-light": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/decode-bmp": { "version": "0.2.1", @@ -6266,16 +6503,6 @@ "node": ">=8.6" } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -6339,6 +6566,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", "engines": { "node": ">=0.4.0" } @@ -6353,9 +6581,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", - "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, "license": "Apache-2.0", "engines": { @@ -6368,45 +6596,11 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "dev": true, - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", - "dev": true, - "peer": true + "dev": true }, "node_modules/dom-helpers": { "version": "5.2.1", @@ -6432,13 +6626,6 @@ "node": ">= 0.4" } }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "dev": true, - "license": "MIT" - }, "node_modules/ejs": { "version": "3.1.10", "resolved": "https://registry.npmjs.org/ejs/-/ejs-3.1.10.tgz", @@ -6456,9 +6643,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.129", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.129.tgz", - "integrity": "sha512-JlXUemX4s0+9f8mLqib/bHH8gOHf5elKS6KeWG3sk3xozb/JTq/RLXIv8OKUWiK4Ah00Wm88EFj5PYkFr4RUPA==", + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", "dev": true, "license": "ISC" }, @@ -6470,30 +6657,54 @@ "license": "MIT" }, "node_modules/engine.io-client": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.4.tgz", - "integrity": "sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==", + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz", + "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==", "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", + "debug": "~4.4.1", "engine.io-parser": "~5.2.1", - "ws": "~8.17.1", - "xmlhttprequest-ssl": "~2.0.0" + "ws": "~8.18.3", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } } }, + "node_modules/engine.io-client/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/engine.io-parser": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.2.tgz", - "integrity": "sha512-RcyUFKA93/CXH20l4SoVvzZfrSDMOTUS3bWVpTt2FuFP+XYrL8i8oonHP7WInRyVHXh0n/ORtoeiE1os+8qkSw==", + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", "engines": { "node": ">=10.0.0" } }, "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -6504,9 +6715,9 @@ } }, "node_modules/es-abstract": { - "version": "1.23.9", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.9.tgz", - "integrity": "sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", "dev": true, "license": "MIT", "dependencies": { @@ -6514,18 +6725,18 @@ "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", - "es-object-atoms": "^1.0.0", + "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", - "get-intrinsic": "^1.2.7", - "get-proto": "^1.0.0", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", @@ -6537,21 +6748,24 @@ "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", + "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", - "is-weakref": "^1.1.0", + "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", - "object-inspect": "^1.13.3", + "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", - "regexp.prototype.flags": "^1.5.3", + "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", @@ -6560,7 +6774,7 @@ "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", - "which-typed-array": "^1.1.18" + "which-typed-array": "^1.1.19" }, "engines": { "node": ">= 0.4" @@ -6588,9 +6802,9 @@ } }, "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", "dev": true, "license": "MIT" }, @@ -6639,10 +6853,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-toolkit": { + "version": "1.45.1", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.45.1.tgz", + "integrity": "sha512-/jhoOj/Fx+A+IIyDNOvO3TItGmlMKhtX8ISAHKE90c4b/k1tqaqEZ+uUqfpU8DMnW5cgNJv606zS55jGvza0Xw==", + "dev": true, + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks" + ] + }, "node_modules/esbuild": { - "version": "0.25.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.2.tgz", - "integrity": "sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==", + "version": "0.27.4", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -6653,31 +6878,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.2", - "@esbuild/android-arm": "0.25.2", - "@esbuild/android-arm64": "0.25.2", - "@esbuild/android-x64": "0.25.2", - "@esbuild/darwin-arm64": "0.25.2", - "@esbuild/darwin-x64": "0.25.2", - "@esbuild/freebsd-arm64": "0.25.2", - "@esbuild/freebsd-x64": "0.25.2", - "@esbuild/linux-arm": "0.25.2", - "@esbuild/linux-arm64": "0.25.2", - "@esbuild/linux-ia32": "0.25.2", - "@esbuild/linux-loong64": "0.25.2", - "@esbuild/linux-mips64el": "0.25.2", - "@esbuild/linux-ppc64": "0.25.2", - "@esbuild/linux-riscv64": "0.25.2", - "@esbuild/linux-s390x": "0.25.2", - "@esbuild/linux-x64": "0.25.2", - "@esbuild/netbsd-arm64": "0.25.2", - "@esbuild/netbsd-x64": "0.25.2", - "@esbuild/openbsd-arm64": "0.25.2", - "@esbuild/openbsd-x64": "0.25.2", - "@esbuild/sunos-x64": "0.25.2", - "@esbuild/win32-arm64": "0.25.2", - "@esbuild/win32-ia32": "0.25.2", - "@esbuild/win32-x64": "0.25.2" + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" } }, "node_modules/escalade": { @@ -6703,86 +6929,95 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.1.0.tgz", + "integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==", "dev": true, + "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.3", + "@eslint/config-helpers": "^0.5.3", + "@eslint/core": "^1.1.1", + "@eslint/plugin-kit": "^0.6.1", + "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", - "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", - "ajv": "^6.12.4", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", - "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.1.2", + "minimatch": "^10.2.4", "natural-compare": "^1.4.0", - "optionator": "^0.9.3", - "strip-ansi": "^6.0.1", - "text-table": "^0.2.0" + "optionator": "^0.9.3" }, "bin": { "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-plugin-react-hooks": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz", - "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz", + "integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==", "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.24.4", + "@babel/parser": "^7.24.4", + "hermes-parser": "^0.25.1", + "zod": "^3.25.0 || ^4.0.0", + "zod-validation-error": "^3.5.0 || ^4.0.0" + }, "engines": { - "node": ">=10" + "node": ">=18" }, "peerDependencies": { - "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0" } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.7", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.7.tgz", - "integrity": "sha512-yrj+KInFmwuQS2UQcg1SF83ha1tuHC1jMQbRNyuWtlEzzKRDgAl7L4Yp4NlDUZTZNlWvHEzOtJhMi40R7JxcSw==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.5.2.tgz", + "integrity": "sha512-hmgTH57GfzoTFjVN0yBwTggnsVUF2tcqi7RJZHqi9lIezSs4eFyAMktA68YD4r5kNw1mxyY4dmkyoFDb3FIqrA==", "dev": true, "license": "MIT", "peerDependencies": { - "eslint": ">=7" + "eslint": "^9 || ^10" } }, "node_modules/eslint-plugin-simple-import-sort": { - "version": "12.1.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.0.tgz", - "integrity": "sha512-Y2fqAfC11TcG/WP3TrI1Gi3p3nc8XJyEOJYHyEPEGI/UAgNx6akxxlX74p7SbAQdLcgASKhj8M0GKvH3vq/+ig==", + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-simple-import-sort/-/eslint-plugin-simple-import-sort-12.1.1.tgz", + "integrity": "sha512-6nuzu4xwQtE3332Uz0to+TxDQYRLTKRESSc2hefVT48Zc8JthmN23Gx9lnYhu0FtkRSL1oxny3kJ2aveVhmOVA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -6790,32 +7025,36 @@ } }, "node_modules/eslint-plugin-testing-library": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-6.2.0.tgz", - "integrity": "sha512-+LCYJU81WF2yQ+Xu4A135CgK8IszcFcyMF4sWkbiu6Oj+Nel0TrkZq/HvDw0/1WuO3dhDQsZA/OpEMGd0NfcUw==", + "version": "7.16.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-testing-library/-/eslint-plugin-testing-library-7.16.2.tgz", + "integrity": "sha512-8gleGnQXK2ZA3hHwjCwpYTZvM+9VsrJ+/9kDI8CjqAQGAdMQOdn/rJNu7ZySENuiWlGKQWyZJ4ZjEg2zamaRHw==", "dev": true, + "license": "MIT", "dependencies": { - "@typescript-eslint/utils": "^5.58.0" + "@typescript-eslint/scope-manager": "^8.56.0", + "@typescript-eslint/utils": "^8.56.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0", - "npm": ">=6" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "peerDependencies": { - "eslint": "^7.5.0 || ^8.0.0" + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0" } }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -6826,6 +7065,7 @@ "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -6833,55 +7073,56 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, + "license": "Apache-2.0", "engines": { - "node": ">=8" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, "engines": { - "node": ">=10" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" } }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, - "dependencies": { - "acorn": "^8.9.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" - }, + "license": "Apache-2.0", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/esquery": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", - "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "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": { "estraverse": "^5.1.0" }, @@ -6894,6 +7135,7 @@ "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "estraverse": "^5.2.0" }, @@ -6906,6 +7148,7 @@ "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=4.0" } @@ -6930,54 +7173,34 @@ } }, "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", "dev": true, - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } + "license": "MIT" }, "node_modules/expect": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", - "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz", + "integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/expect-utils": "^29.7.0", - "jest-get-type": "^29.6.3", - "jest-matcher-utils": "^29.7.0", - "jest-message-util": "^29.7.0", - "jest-util": "^29.7.0" + "@jest/expect-utils": "30.3.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.3.0", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/expect-type": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", - "integrity": "sha512-/kP8CAwxzLVEeFrMm4kMmy4CCDlpipyA7MYLVrdJIkV0fYF0UaigQHRsxHiuY/GEea+bh4KSv3TIlgr+2UL6bw==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -6989,44 +7212,6 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" }, - "node_modules/fast-equals": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", - "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/fast-glob": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", - "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", - "dev": true, - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-glob/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -7040,9 +7225,9 @@ "dev": true }, "node_modules/fast-uri": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.6.tgz", - "integrity": "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", "dev": true, "funding": [ { @@ -7053,17 +7238,8 @@ "type": "opencollective", "url": "https://opencollective.com/fastify" } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fastq": { - "version": "1.17.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", - "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", - "dev": true, - "dependencies": { - "reusify": "^1.0.4" - } + ], + "license": "BSD-3-Clause" }, "node_modules/fflate": { "version": "0.8.2", @@ -7073,15 +7249,16 @@ "license": "MIT" }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, + "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/file-selector": { @@ -7097,9 +7274,9 @@ } }, "node_modules/filelist": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.4.tgz", - "integrity": "sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/filelist/-/filelist-1.0.6.tgz", + "integrity": "sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -7117,9 +7294,9 @@ } }, "node_modules/filelist/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", "dev": true, "license": "ISC", "dependencies": { @@ -7158,36 +7335,37 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", - "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", "dev": true, "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", "url": "https://github.com/sponsors/RubenVerborgh" } ], + "license": "MIT", "engines": { "node": ">=4.0" }, @@ -7244,9 +7422,9 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -7259,11 +7437,31 @@ "node": ">= 6" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs-extra/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } }, "node_modules/fsevents": { "version": "2.3.3", @@ -7319,6 +7517,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -7391,18 +7599,6 @@ "node": ">= 0.4" } }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/get-symbol-description": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", @@ -7421,26 +7617,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -7454,12 +7630,16 @@ } }, "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "version": "17.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", + "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globalthis": { @@ -7479,26 +7659,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "dev": true, - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -7517,16 +7677,10 @@ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "dev": true }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true - }, "node_modules/graphql": { - "version": "16.10.0", - "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.10.0.tgz", - "integrity": "sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==", + "version": "16.13.2", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.13.2.tgz", + "integrity": "sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==", "dev": true, "license": "MIT", "engines": { @@ -7630,23 +7784,42 @@ "dev": true, "license": "MIT" }, + "node_modules/hermes-estree": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz", + "integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==", + "dev": true, + "license": "MIT" + }, + "node_modules/hermes-parser": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz", + "integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hermes-estree": "0.25.1" + } + }, "node_modules/html-encoding-sniffer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", - "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", "dev": true, + "license": "MIT", "dependencies": { - "whatwg-encoding": "^3.1.1" + "@exodus/bytes": "^1.6.0" }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/http-proxy-agent": { "version": "7.0.2", @@ -7675,22 +7848,14 @@ "node": ">= 14" } }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "dev": true, - "engines": { - "node": ">=10.17.0" - } - }, "node_modules/husky": { - "version": "9.0.11", - "resolved": "https://registry.npmjs.org/husky/-/husky-9.0.11.tgz", - "integrity": "sha512-AB6lFlbwwyIqMdHYhwPe+kjOC3Oc5P3nThEoW/AaO2BX3vJDjWPFxYLxokUZOo6RNX20He3AaT8sESs9NJcmEw==", + "version": "9.1.7", + "resolved": "https://registry.npmjs.org/husky/-/husky-9.1.7.tgz", + "integrity": "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==", "dev": true, + "license": "MIT", "bin": { - "husky": "bin.mjs" + "husky": "bin.js" }, "engines": { "node": ">=18" @@ -7706,18 +7871,6 @@ "dev": true, "license": "MPL-2.0" }, - "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/idb": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", @@ -7734,22 +7887,24 @@ "node": ">= 4" } }, - "node_modules/import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "node_modules/immer": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", "dev": true, - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, + "license": "MIT", "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "opencollective", + "url": "https://opencollective.com/immer" } }, + "node_modules/immutable": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", + "dev": true, + "license": "MIT" + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -7768,22 +7923,6 @@ "node": ">=8" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -7804,6 +7943,7 @@ "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", "dev": true, + "license": "ISC", "engines": { "node": ">=12" } @@ -7979,14 +8119,15 @@ } }, "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" }, @@ -8029,6 +8170,19 @@ "dev": true, "license": "MIT" }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-node-process": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz", @@ -8072,15 +8226,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -8272,6 +8417,7 @@ "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", "dev": true, + "license": "BSD-3-Clause", "engines": { "node": ">=8" } @@ -8281,6 +8427,7 @@ "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", @@ -8290,26 +8437,12 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-source-maps": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", - "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.23", - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/istanbul-reports": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", - "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" @@ -8318,33 +8451,16 @@ "node": ">=8" } }, - "node_modules/jackspeak": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", - "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "@isaacs/cliui": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - }, - "optionalDependencies": { - "@pkgjs/parseargs": "^0.11.0" - } - }, "node_modules/jake": { - "version": "10.9.2", - "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.2.tgz", - "integrity": "sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA==", + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/jake/-/jake-10.9.4.tgz", + "integrity": "sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "async": "^3.2.3", - "chalk": "^4.0.2", + "async": "^3.2.6", "filelist": "^1.0.4", - "minimatch": "^3.1.2" + "picocolors": "^1.1.1" }, "bin": { "jake": "bin/cli.js" @@ -8354,18 +8470,19 @@ } }, "node_modules/jest-diff": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", - "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz", + "integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==", "dev": true, + "license": "MIT", "dependencies": { - "chalk": "^4.0.0", - "diff-sequences": "^29.6.3", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" + "@jest/diff-sequences": "30.3.0", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.3.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-diff/node_modules/ansi-styles": { @@ -8373,6 +8490,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -8381,47 +8499,41 @@ } }, "node_modules/jest-diff/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-diff/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, - "node_modules/jest-get-type": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", - "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } + "license": "MIT" }, "node_modules/jest-matcher-utils": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", - "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz", + "integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==", "dev": true, + "license": "MIT", "dependencies": { - "chalk": "^4.0.0", - "jest-diff": "^29.7.0", - "jest-get-type": "^29.6.3", - "pretty-format": "^29.7.0" + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.3.0", + "pretty-format": "30.3.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-matcher-utils/node_modules/ansi-styles": { @@ -8429,6 +8541,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -8437,92 +8550,150 @@ } }, "node_modules/jest-matcher-utils/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-matcher-utils/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" }, "node_modules/jest-message-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", - "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.12.13", - "@jest/types": "^29.6.3", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.9", - "micromatch": "^4.0.4", - "pretty-format": "^29.7.0", + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.3.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", "slash": "^3.0.0", - "stack-utils": "^2.0.3" + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-mock": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-util": "30.3.0" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-message-util/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", "dev": true, + "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-message-util/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "node_modules/jest-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", + "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", "dev": true, + "license": "MIT", "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "node_modules/jest-message-util/node_modules/react-is": { - "version": "18.2.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", - "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==", - "dev": true - }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, + "license": "MIT", "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/jiti": { @@ -8540,50 +8711,37 @@ "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, - "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, "node_modules/jsdom": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.0.0.tgz", - "integrity": "sha512-BZYDGVAIriBWTpIxYzrXjv3E/4u8+/pSG5bQdIYCbNCGOvsPkDQfTVLAIXAf9ETdCpduCVTkDe2NNZ8NIwUVzw==", + "version": "28.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-28.1.0.tgz", + "integrity": "sha512-0+MoQNYyr2rBHqO1xilltfDjV9G7ymYGlAUazgcDLQaUf8JDHbuGwsxN6U9qWaElZ4w1B2r7yEGIL3GdeW3Rug==", "dev": true, "license": "MIT", "dependencies": { - "cssstyle": "^4.2.1", - "data-urls": "^5.0.0", - "decimal.js": "^10.4.3", - "form-data": "^4.0.1", - "html-encoding-sniffer": "^4.0.0", + "@acemir/cssom": "^0.9.31", + "@asamuzakjp/dom-selector": "^6.8.1", + "@bramus/specificity": "^2.4.2", + "@exodus/bytes": "^1.11.0", + "cssstyle": "^6.0.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", "http-proxy-agent": "^7.0.2", "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.16", - "parse5": "^7.2.1", - "rrweb-cssom": "^0.8.0", + "parse5": "^8.0.0", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^5.0.0", + "tough-cookie": "^6.0.0", + "undici": "^7.21.0", "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^7.0.0", - "whatwg-encoding": "^3.1.1", - "whatwg-mimetype": "^4.0.0", - "whatwg-url": "^14.1.0", - "ws": "^8.18.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0", "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { "canvas": "^3.0.0" @@ -8594,28 +8752,6 @@ } } }, - "node_modules/jsdom/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -8633,7 +8769,8 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-schema": { "version": "0.4.0", @@ -8646,7 +8783,8 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", @@ -8667,10 +8805,11 @@ } }, "node_modules/jsonfile": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", - "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", "dev": true, + "license": "MIT", "dependencies": { "universalify": "^2.0.0" }, @@ -8703,6 +8842,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -8754,10 +8894,11 @@ } }, "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" }, "node_modules/lodash.debounce": { "version": "4.0.8", @@ -8766,12 +8907,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true - }, "node_modules/lodash.sortby": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/lodash.sortby/-/lodash.sortby-4.7.0.tgz", @@ -8790,13 +8925,6 @@ "loose-envify": "cli.js" } }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -8811,31 +8939,30 @@ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, - "peer": true, "bin": { "lz-string": "bin/bin.js" } }, "node_modules/magic-string": { - "version": "0.30.17", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", - "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0" + "@jridgewell/sourcemap-codec": "^1.5.5" } }, "node_modules/magicast": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", - "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", + "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", "dev": true, "license": "MIT", "dependencies": { - "@babel/parser": "^7.25.4", - "@babel/types": "^7.25.4", - "source-map-js": "^1.2.0" + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "source-map-js": "^1.2.1" } }, "node_modules/make-dir": { @@ -8843,6 +8970,7 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", "dev": true, + "license": "MIT", "dependencies": { "semver": "^7.5.3" }, @@ -8853,26 +8981,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-dir/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/make-dir/node_modules/semver": { - "version": "7.6.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", - "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -8880,12 +8994,6 @@ "node": ">=10" } }, - "node_modules/make-dir/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -8895,39 +9003,18 @@ "node": ">= 0.4" } }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "dev": true, - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } + "license": "CC0-1.0" }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -8936,6 +9023,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", "dependencies": { "mime-db": "1.52.0" }, @@ -8943,15 +9031,6 @@ "node": ">= 0.6" } }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/min-indent": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", @@ -8962,15 +9041,19 @@ } }, "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^1.1.7" + "brace-expansion": "^5.0.2" }, "engines": { - "node": "*" + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/minipass": { @@ -9008,30 +9091,30 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/msw": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/msw/-/msw-2.7.0.tgz", - "integrity": "sha512-BIodwZ19RWfCbYTxWTUfTXc+sg4OwjCAgxU1ZsgmggX/7S3LdUifsbUPJs61j0rWb19CZRGY5if77duhc0uXzw==", + "version": "2.12.14", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.12.14.tgz", + "integrity": "sha512-4KXa4nVBIBjbDbd7vfQNuQ25eFxug0aropCQFoI0JdOBuJWamkT1yLVIWReFI8SiTRc+H1hKzaNk+cLk2N9rtQ==", "dev": true, "hasInstallScript": true, "license": "MIT", "dependencies": { - "@bundled-es-modules/cookie": "^2.0.1", - "@bundled-es-modules/statuses": "^1.0.1", - "@bundled-es-modules/tough-cookie": "^0.1.6", "@inquirer/confirm": "^5.0.0", - "@mswjs/interceptors": "^0.37.0", + "@mswjs/interceptors": "^0.41.2", "@open-draft/deferred-promise": "^2.2.0", - "@open-draft/until": "^2.1.0", - "@types/cookie": "^0.6.0", - "@types/statuses": "^2.0.4", - "graphql": "^16.8.1", + "@types/statuses": "^2.0.6", + "cookie": "^1.0.2", + "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", + "rettime": "^0.10.1", + "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", - "type-fest": "^4.26.1", + "tough-cookie": "^6.0.0", + "type-fest": "^5.2.0", + "until-async": "^3.0.2", "yargs": "^17.7.2" }, "bin": { @@ -9053,13 +9136,16 @@ } }, "node_modules/msw/node_modules/type-fest": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.32.0.tgz", - "integrity": "sha512-rfgpoi08xagF3JSdtJlCwMq9DGNDE0IMh3Mkpc1wUypg9vPi786AiqeBBKcqvIkq42azsBM85N490fyZjeUftw==", + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-5.5.0.tgz", + "integrity": "sha512-PlBfpQwiUvGViBNX84Yxwjsdhd1TUlXr6zjX7eoirtCPIr08NAmxwa+fcYBTeRQxHo9YC9wwF3m9i700sHma8g==", "dev": true, "license": "(MIT OR CC0-1.0)", + "dependencies": { + "tagged-tag": "^1.0.0" + }, "engines": { - "node": ">=16" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -9100,29 +9186,18 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", - "dev": true, - "license": "MIT" - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "dev": true, - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } + "license": "MIT", + "optional": true }, - "node_modules/nwsapi": { - "version": "2.2.16", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.16.tgz", - "integrity": "sha512-F1I/bimDpj3ncaNDhfyMWuFqmQDBwDB0Fogc2qpL3BWvkQteFD/8BzWuIRl83rq0DXfm8SGt/HFhLXZyljTXcQ==", + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", "dev": true, "license": "MIT" }, @@ -9178,29 +9253,16 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", "dev": true, - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" }, "node_modules/optionator": { "version": "0.9.4", @@ -9282,26 +9344,14 @@ "dev": true, "license": "BlueOak-1.0.0" }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "dev": true, - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, "node_modules/parse5": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", - "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", "dev": true, "license": "MIT", "dependencies": { - "entities": "^4.5.0" + "entities": "^6.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" @@ -9316,15 +9366,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -9338,31 +9379,8 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "dev": true - }, - "node_modules/path-scurry": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", - "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", - "dev": true, - "license": "BlueOak-1.0.0", - "dependencies": { - "lru-cache": "^10.2.0", - "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" - }, - "engines": { - "node": ">=16 || 14 >=14.18" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.4.3", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", - "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "license": "ISC" + "license": "MIT" }, "node_modules/path-to-regexp": { "version": "6.3.0", @@ -9371,15 +9389,6 @@ "dev": true, "license": "MIT" }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/pathe": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", @@ -9387,16 +9396,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -9584,10 +9583,11 @@ } }, "node_modules/prettier": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.2.5.tgz", - "integrity": "sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==", + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", "dev": true, + "license": "MIT", "bin": { "prettier": "bin/prettier.cjs" }, @@ -9599,21 +9599,18 @@ } }, "node_modules/prettier-plugin-organize-imports": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-3.2.4.tgz", - "integrity": "sha512-6m8WBhIp0dfwu0SkgfOxJqh+HpdyfqSSLfKKRZSFbDuEQXDDndb8fTpRWkUrX/uBenkex3MgnVk0J3b3Y5byog==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-organize-imports/-/prettier-plugin-organize-imports-4.3.0.tgz", + "integrity": "sha512-FxFz0qFhyBsGdIsb697f/EkvHzi5SZOhWAjxcx2dLt+Q532bAlhswcXGYB1yzjZ69kW8UoadFBw7TyNwlq96Iw==", "dev": true, + "license": "MIT", "peerDependencies": { - "@volar/vue-language-plugin-pug": "^1.0.4", - "@volar/vue-typescript": "^1.0.4", "prettier": ">=2.0", - "typescript": ">=2.9" + "typescript": ">=2.9", + "vue-tsc": "^2.1.0 || 3" }, "peerDependenciesMeta": { - "@volar/vue-language-plugin-pug": { - "optional": true - }, - "@volar/vue-typescript": { + "vue-tsc": { "optional": true } } @@ -9636,7 +9633,6 @@ "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", "dev": true, - "peer": true, "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", @@ -9651,7 +9647,6 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, - "peer": true, "engines": { "node": ">=10" }, @@ -9663,22 +9658,22 @@ "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "peer": true + "dev": true }, "node_modules/pretty-quick": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pretty-quick/-/pretty-quick-4.0.0.tgz", - "integrity": "sha512-M+2MmeufXb/M7Xw3Afh1gxcYpj+sK0AxEfnfF958ktFeAyi5MsKY5brymVURQLgPLV1QaF5P4pb2oFJ54H3yzQ==", + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/pretty-quick/-/pretty-quick-4.2.2.tgz", + "integrity": "sha512-uAh96tBW1SsD34VhhDmWuEmqbpfYc/B3j++5MC/6b3Cb8Ow7NJsvKFhg0eoGu2xXX+o9RkahkTK6sUdd8E7g5w==", "dev": true, + "license": "MIT", "dependencies": { - "execa": "^5.1.1", - "find-up": "^5.0.0", - "ignore": "^5.3.0", + "@pkgr/core": "^0.2.7", + "ignore": "^7.0.5", "mri": "^1.2.0", - "picocolors": "^1.0.0", - "picomatch": "^3.0.1", - "tslib": "^2.6.2" + "picocolors": "^1.1.1", + "picomatch": "^4.0.2", + "tinyexec": "^0.3.2", + "tslib": "^2.8.1" }, "bin": { "pretty-quick": "lib/cli.mjs" @@ -9686,17 +9681,31 @@ "engines": { "node": ">=14" }, + "funding": { + "url": "https://opencollective.com/pretty-quick" + }, "peerDependencies": { "prettier": "^3.0.0" } }, + "node_modules/pretty-quick/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/pretty-quick/node_modules/picomatch": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", - "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, + "license": "MIT", "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -9713,21 +9722,12 @@ } }, "node_modules/proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" - }, - "node_modules/psl": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", - "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", - "dev": true, + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "funding": { - "url": "https://github.com/sponsors/lupomontero" + "engines": { + "node": ">=10" } }, "node_modules/punycode": { @@ -9746,78 +9746,41 @@ "dev": true, "funding": [ { - "type": "individual", - "url": "https://github.com/sponsors/antfu" - }, - { - "type": "individual", - "url": "https://github.com/sponsors/sxzz" - } - ], - "license": "MIT" - }, - "node_modules/querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" + "type": "individual", + "url": "https://github.com/sponsors/antfu" }, { - "type": "consulting", - "url": "https://feross.org/support" + "type": "individual", + "url": "https://github.com/sponsors/sxzz" } - ] - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } + ], + "license": "MIT" }, "node_modules/react": { - "version": "19.2.3", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", - "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", + "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "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==", + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", + "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.3" + "react": "^19.2.4" } }, "node_modules/react-dropzone": { - "version": "14.3.8", - "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz", - "integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-15.0.0.tgz", + "integrity": "sha512-lGjYV/EoqEjEWPnmiSvH4v5IoIAwQM2W4Z1C0Q/Pw2xD0eVzKPS359BQTUMum+1fa0kH2nrKjuavmTPOGhpLPg==", "license": "MIT", "dependencies": { "attr-accept": "^2.2.4", @@ -9846,10 +9809,34 @@ "react-dom": "^0.14 || ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, + "node_modules/react-redux": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", + "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/use-sync-external-store": "^0.0.6", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "@types/react": "^18.2.25 || ^19", + "react": "^18.0 || ^19", + "redux": "^5.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "redux": { + "optional": true + } + } + }, "node_modules/react-refresh": { - "version": "0.17.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", - "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", "dev": true, "license": "MIT", "engines": { @@ -9904,14 +9891,13 @@ } }, "node_modules/react-router": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.5.2.tgz", - "integrity": "sha512-9Rw8r199klMnlGZ8VAsV/I8WrIF6IyJ90JQUdboupx1cdkgYqwnrYjH+I/nY/7cA1X5zia4mDJqH36npP7sxGQ==", + "version": "7.13.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.2.tgz", + "integrity": "sha512-tX1Aee+ArlKQP+NIUd7SE6Li+CiGKwQtbS+FfRxPX6Pe4vHOo6nr9d++u5cwg+Z8K/x8tP+7qLmujDtfrAoUJA==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", - "set-cookie-parser": "^2.6.0", - "turbo-stream": "2.4.0" + "set-cookie-parser": "^2.6.0" }, "engines": { "node": ">=20.0.0" @@ -9926,22 +9912,6 @@ } } }, - "node_modules/react-smooth": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", - "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-equals": "^5.0.1", - "prop-types": "^15.8.1", - "react-transition-group": "^4.4.5" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -10006,46 +9976,51 @@ "react-dom": ">=16.6.0" } }, - "node_modules/recharts": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.0.tgz", - "integrity": "sha512-cIvMxDfpAmqAmVgc4yb7pgm/O1tmmkl/CjrvXuW+62/+7jj/iF9Ykm+hb/UJt42TREHMyd3gb+pkgoa2MxgDIw==", + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, "license": "MIT", - "dependencies": { - "clsx": "^2.0.0", - "eventemitter3": "^4.0.1", - "lodash": "^4.17.21", - "react-is": "^18.3.1", - "react-smooth": "^4.0.0", - "recharts-scale": "^0.4.4", - "tiny-invariant": "^1.3.1", - "victory-vendor": "^36.6.8" - }, "engines": { - "node": ">=14" + "node": ">= 14.18.0" }, - "peerDependencies": { - "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "node_modules/recharts-scale": { - "version": "0.4.5", - "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", - "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "node_modules/recharts": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-3.8.1.tgz", + "integrity": "sha512-mwzmO1s9sFL0TduUpwndxCUNoXsBw3u3E/0+A+cLcrSfQitSG62L32N69GhqUrrT5qKcAE3pCGVINC6pqkBBQg==", "dev": true, + "license": "MIT", + "workspaces": [ + "www" + ], "dependencies": { - "decimal.js-light": "^2.4.1" + "@reduxjs/toolkit": "^1.9.0 || 2.x.x", + "clsx": "^2.1.1", + "decimal.js-light": "^2.5.1", + "es-toolkit": "^1.39.3", + "eventemitter3": "^5.0.1", + "immer": "^10.1.1", + "react-redux": "8.x.x || 9.x.x", + "reselect": "5.1.1", + "tiny-invariant": "^1.3.3", + "use-sync-external-store": "^1.2.2", + "victory-vendor": "^37.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-is": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, - "node_modules/recharts/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true, - "license": "MIT" - }, "node_modules/redent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", @@ -10059,6 +10034,23 @@ "node": ">=8" } }, + "node_modules/redux": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", + "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", + "dev": true, + "license": "MIT" + }, + "node_modules/redux-thunk": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", + "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "redux": "^5.0.0" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -10090,9 +10082,9 @@ "license": "MIT" }, "node_modules/regenerate-unicode-properties": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", - "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", "dev": true, "license": "MIT", "dependencies": { @@ -10107,16 +10099,6 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" }, - "node_modules/regenerator-transform": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", - "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, "node_modules/regexp.prototype.flags": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", @@ -10139,18 +10121,18 @@ } }, "node_modules/regexpu-core": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", - "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", "dev": true, "license": "MIT", "dependencies": { "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.0", + "regenerate-unicode-properties": "^10.2.2", "regjsgen": "^0.8.0", - "regjsparser": "^0.12.0", + "regjsparser": "^0.13.0", "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" + "unicode-match-property-value-ecmascript": "^2.2.1" }, "engines": { "node": ">=4" @@ -10164,28 +10146,18 @@ "license": "MIT" }, "node_modules/regjsparser": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", - "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "jsesc": "~3.0.2" + "jsesc": "~3.1.0" }, "bin": { "regjsparser": "bin/parser" } }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -10206,21 +10178,21 @@ "node": ">=0.10.0" } }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "node_modules/reselect": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", + "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", "dev": true, "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "dev": true, "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -10234,44 +10206,17 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true, - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "node_modules/rettime": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/rettime/-/rettime-0.10.1.tgz", + "integrity": "sha512-uyDrIlUEH37cinabq0AX4QbgV4HbFZ/gqoiunWQ1UqBtRvTTytwhNYjE++pO/MjPTZL5KQCf2bEoJ/BJNVQ5Kw==", "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } + "license": "MIT" }, "node_modules/rollup": { - "version": "4.48.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.48.1.tgz", - "integrity": "sha512-jVG20NvbhTYDkGAty2/Yh7HK6/q3DGSRH4o8ALKGArmMuaauM9kLfoMZ+WliPwA5+JHr2lTn3g557FxBV87ifg==", + "version": "4.59.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", + "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", "dev": true, "license": "MIT", "dependencies": { @@ -10285,59 +10230,34 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.48.1", - "@rollup/rollup-android-arm64": "4.48.1", - "@rollup/rollup-darwin-arm64": "4.48.1", - "@rollup/rollup-darwin-x64": "4.48.1", - "@rollup/rollup-freebsd-arm64": "4.48.1", - "@rollup/rollup-freebsd-x64": "4.48.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.48.1", - "@rollup/rollup-linux-arm-musleabihf": "4.48.1", - "@rollup/rollup-linux-arm64-gnu": "4.48.1", - "@rollup/rollup-linux-arm64-musl": "4.48.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.48.1", - "@rollup/rollup-linux-ppc64-gnu": "4.48.1", - "@rollup/rollup-linux-riscv64-gnu": "4.48.1", - "@rollup/rollup-linux-riscv64-musl": "4.48.1", - "@rollup/rollup-linux-s390x-gnu": "4.48.1", - "@rollup/rollup-linux-x64-gnu": "4.48.1", - "@rollup/rollup-linux-x64-musl": "4.48.1", - "@rollup/rollup-win32-arm64-msvc": "4.48.1", - "@rollup/rollup-win32-ia32-msvc": "4.48.1", - "@rollup/rollup-win32-x64-msvc": "4.48.1", + "@rollup/rollup-android-arm-eabi": "4.59.0", + "@rollup/rollup-android-arm64": "4.59.0", + "@rollup/rollup-darwin-arm64": "4.59.0", + "@rollup/rollup-darwin-x64": "4.59.0", + "@rollup/rollup-freebsd-arm64": "4.59.0", + "@rollup/rollup-freebsd-x64": "4.59.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", + "@rollup/rollup-linux-arm-musleabihf": "4.59.0", + "@rollup/rollup-linux-arm64-gnu": "4.59.0", + "@rollup/rollup-linux-arm64-musl": "4.59.0", + "@rollup/rollup-linux-loong64-gnu": "4.59.0", + "@rollup/rollup-linux-loong64-musl": "4.59.0", + "@rollup/rollup-linux-ppc64-gnu": "4.59.0", + "@rollup/rollup-linux-ppc64-musl": "4.59.0", + "@rollup/rollup-linux-riscv64-gnu": "4.59.0", + "@rollup/rollup-linux-riscv64-musl": "4.59.0", + "@rollup/rollup-linux-s390x-gnu": "4.59.0", + "@rollup/rollup-linux-x64-gnu": "4.59.0", + "@rollup/rollup-linux-x64-musl": "4.59.0", + "@rollup/rollup-openbsd-x64": "4.59.0", + "@rollup/rollup-openharmony-arm64": "4.59.0", + "@rollup/rollup-win32-arm64-msvc": "4.59.0", + "@rollup/rollup-win32-ia32-msvc": "4.59.0", + "@rollup/rollup-win32-x64-gnu": "4.59.0", + "@rollup/rollup-win32-x64-msvc": "4.59.0", "fsevents": "~2.3.2" } }, - "node_modules/rrweb-cssom": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", - "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", - "dev": true, - "license": "MIT" - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, "node_modules/rxjs": { "version": "7.8.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", @@ -10368,27 +10288,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, "node_modules/safe-push-apply": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", @@ -10424,23 +10323,38 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "node_modules/sass": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz", + "integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } }, "node_modules/sass-embedded": { - "version": "1.86.1", - "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.86.1.tgz", - "integrity": "sha512-LMJvytHh7lIUtmjGCqpM4cRdIDvPllLJKznNIK4L7EZJ77BLeUFoOSRXEOHq4G4gqy5CVhHUKlHslzCANkDOhQ==", + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.98.0.tgz", + "integrity": "sha512-Do7u6iRb6K+lrllcTkB1BXcHwOxcKe3rEfOF/GcCLE2w3WpddakRAosJOHFUR37DpsvimQXEt5abs3NzUjEIqg==", "dev": true, "license": "MIT", "dependencies": { - "@bufbuild/protobuf": "^2.0.0", - "buffer-builder": "^0.2.0", + "@bufbuild/protobuf": "^2.5.0", "colorjs.io": "^0.5.0", - "immutable": "^5.0.2", + "immutable": "^5.1.5", "rxjs": "^7.4.0", "supports-color": "^8.1.1", "sync-child-process": "^1.0.2", @@ -10453,51 +10367,49 @@ "node": ">=16.0.0" }, "optionalDependencies": { - "sass-embedded-android-arm": "1.86.1", - "sass-embedded-android-arm64": "1.86.1", - "sass-embedded-android-ia32": "1.86.1", - "sass-embedded-android-riscv64": "1.86.1", - "sass-embedded-android-x64": "1.86.1", - "sass-embedded-darwin-arm64": "1.86.1", - "sass-embedded-darwin-x64": "1.86.1", - "sass-embedded-linux-arm": "1.86.1", - "sass-embedded-linux-arm64": "1.86.1", - "sass-embedded-linux-ia32": "1.86.1", - "sass-embedded-linux-musl-arm": "1.86.1", - "sass-embedded-linux-musl-arm64": "1.86.1", - "sass-embedded-linux-musl-ia32": "1.86.1", - "sass-embedded-linux-musl-riscv64": "1.86.1", - "sass-embedded-linux-musl-x64": "1.86.1", - "sass-embedded-linux-riscv64": "1.86.1", - "sass-embedded-linux-x64": "1.86.1", - "sass-embedded-win32-arm64": "1.86.1", - "sass-embedded-win32-ia32": "1.86.1", - "sass-embedded-win32-x64": "1.86.1" - } - }, - "node_modules/sass-embedded-android-arm": { - "version": "1.86.1", - "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.86.1.tgz", - "integrity": "sha512-bcmKB67uCb9znune+QsE6cWIiKAHE9P+24/9vDPHwwN3BmmH1B/4mznNKKakdYMuxpgbeLrPcEScHEpQbdrIpA==", + "sass-embedded-all-unknown": "1.98.0", + "sass-embedded-android-arm": "1.98.0", + "sass-embedded-android-arm64": "1.98.0", + "sass-embedded-android-riscv64": "1.98.0", + "sass-embedded-android-x64": "1.98.0", + "sass-embedded-darwin-arm64": "1.98.0", + "sass-embedded-darwin-x64": "1.98.0", + "sass-embedded-linux-arm": "1.98.0", + "sass-embedded-linux-arm64": "1.98.0", + "sass-embedded-linux-musl-arm": "1.98.0", + "sass-embedded-linux-musl-arm64": "1.98.0", + "sass-embedded-linux-musl-riscv64": "1.98.0", + "sass-embedded-linux-musl-x64": "1.98.0", + "sass-embedded-linux-riscv64": "1.98.0", + "sass-embedded-linux-x64": "1.98.0", + "sass-embedded-unknown-all": "1.98.0", + "sass-embedded-win32-arm64": "1.98.0", + "sass-embedded-win32-x64": "1.98.0" + } + }, + "node_modules/sass-embedded-all-unknown": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-all-unknown/-/sass-embedded-all-unknown-1.98.0.tgz", + "integrity": "sha512-6n4RyK7/1mhdfYvpP3CClS3fGoYqDvRmLClCESS6I7+SAzqjxvGG6u5Fo+cb1nrPNbbilgbM4QKdgcgWHO9NCA==", "cpu": [ - "arm" + "!arm", + "!arm64", + "!riscv64", + "!x64" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=14.0.0" + "dependencies": { + "sass": "1.98.0" } }, - "node_modules/sass-embedded-android-arm64": { - "version": "1.86.1", - "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.86.1.tgz", - "integrity": "sha512-SMY79YhNfq/gdz8MHqwEsnf/IjSnQFAmSEGDDv0vjL0yy9VZC/zhsxpsho8vbFEvTSEGFFlkGgPdzDuoozRrOg==", + "node_modules/sass-embedded-android-arm": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm/-/sass-embedded-android-arm-1.98.0.tgz", + "integrity": "sha512-LjGiMhHgu7VL1n7EJxTCre1x14bUsWd9d3dnkS2rku003IWOI/fxc7OXgaKagoVzok1kv09rzO3vFXJR5ZeONQ==", "cpu": [ - "arm64" + "arm" ], "dev": true, "license": "MIT", @@ -10509,12 +10421,12 @@ "node": ">=14.0.0" } }, - "node_modules/sass-embedded-android-ia32": { - "version": "1.86.1", - "resolved": "https://registry.npmjs.org/sass-embedded-android-ia32/-/sass-embedded-android-ia32-1.86.1.tgz", - "integrity": "sha512-AX6I5qS8GbgcbBJ1o3uKVI5/7tq6evg/BO/wa0XaNqnzP4i/PojBaGh7EcZrg/spl//SfpS55eA18a0/AOi71w==", + "node_modules/sass-embedded-android-arm64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-arm64/-/sass-embedded-android-arm64-1.98.0.tgz", + "integrity": "sha512-M9Ra98A6vYJHpwhoC/5EuH1eOshQ9ZyNwC8XifUDSbRl/cGeQceT1NReR9wFj3L7s1pIbmes1vMmaY2np0uAKQ==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "license": "MIT", @@ -10527,9 +10439,9 @@ } }, "node_modules/sass-embedded-android-riscv64": { - "version": "1.86.1", - "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.86.1.tgz", - "integrity": "sha512-Af6ZzRTRfIfx6KICJZ19je6OjOXhxo+v6z/lf/SXm5/1EaHGpGC5xIw4ivtj4nNINNoqkykfIDCjpzm1qWEPPQ==", + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-riscv64/-/sass-embedded-android-riscv64-1.98.0.tgz", + "integrity": "sha512-WPe+0NbaJIZE1fq/RfCZANMeIgmy83x4f+SvFOG7LhUthHpZWcOcrPTsCKKmN3xMT3iw+4DXvqTYOCYGRL3hcQ==", "cpu": [ "riscv64" ], @@ -10544,9 +10456,9 @@ } }, "node_modules/sass-embedded-android-x64": { - "version": "1.86.1", - "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.86.1.tgz", - "integrity": "sha512-GW47z1AH8gXB7IG6EUbC5aDBDtiITeP5nUfEenE6vaaN0H17mBjIwSnEcKPPA1IdxzDpj+4bE/SGfiF0W/At4g==", + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-android-x64/-/sass-embedded-android-x64-1.98.0.tgz", + "integrity": "sha512-zrD25dT7OHPEgLWuPEByybnIfx4rnCtfge4clBgjZdZ3lF6E7qNLRBtSBmoFflh6Vg0RlEjJo5VlpnTMBM5MQQ==", "cpu": [ "x64" ], @@ -10561,9 +10473,9 @@ } }, "node_modules/sass-embedded-darwin-arm64": { - "version": "1.86.1", - "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.86.1.tgz", - "integrity": "sha512-grBnDW5Rg+mEmZM7I9hJySS4MMXDwLMd+RyegQnr+SIJ3WA807Cw830+raALxgDY+UKKKhVEoq3FgbTo40Awgw==", + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-arm64/-/sass-embedded-darwin-arm64-1.98.0.tgz", + "integrity": "sha512-cgr1z9rBnCdMf8K+JabIaYd9Rag2OJi5mjq08XJfbJGMZV/TA6hFJCLGkr5/+ZOn4/geTM5/3aSfQ8z5EIJAOg==", "cpu": [ "arm64" ], @@ -10578,9 +10490,9 @@ } }, "node_modules/sass-embedded-darwin-x64": { - "version": "1.86.1", - "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.86.1.tgz", - "integrity": "sha512-XxSCMcmeADNouiJAr8G1oRnEhkivHKVLV5DRpfFnUK5FqtFCuSk3K18I+xIfpQDeZnjRL3t2VjsmEJuFiBYV8w==", + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-darwin-x64/-/sass-embedded-darwin-x64-1.98.0.tgz", + "integrity": "sha512-OLBOCs/NPeiMqTdOrMFbVHBQFj19GS3bSVSxIhcCq16ZyhouUkYJEZjxQgzv9SWA2q6Ki8GCqp4k6jMeUY9dcA==", "cpu": [ "x64" ], @@ -10595,9 +10507,9 @@ } }, "node_modules/sass-embedded-linux-arm": { - "version": "1.86.1", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.86.1.tgz", - "integrity": "sha512-Z57ZUcWPuoOHpnl3TiUf/x9wWF2dFtkjdv7hZQpFXYwK5eudHFeBErK6KNCos6jkif1KyeFELXT/HWOznitU/w==", + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm/-/sass-embedded-linux-arm-1.98.0.tgz", + "integrity": "sha512-03baQZCxVyEp8v1NWBRlzGYrmVT/LK7ZrHlF1piscGiGxwfdxoLXVuxsylx3qn/dD/4i/rh7Bzk7reK1br9jvQ==", "cpu": [ "arm" ], @@ -10612,9 +10524,9 @@ } }, "node_modules/sass-embedded-linux-arm64": { - "version": "1.86.1", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.86.1.tgz", - "integrity": "sha512-zchms0BtaOrkvfvjRnl1PDWK931DxAeYEY2yKQceO/0OFtcBz1r480Kh/RjIffTNreJqIr9Mx4wFdP+icKwLpg==", + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-arm64/-/sass-embedded-linux-arm64-1.98.0.tgz", + "integrity": "sha512-axOE3t2MTBwCtkUCbrdM++Gj0gC0fdHJPrgzQ+q1WUmY9NoNMGqflBtk5mBZaWUeha2qYO3FawxCB8lctFwCtw==", "cpu": [ "arm64" ], @@ -10628,27 +10540,10 @@ "node": ">=14.0.0" } }, - "node_modules/sass-embedded-linux-ia32": { - "version": "1.86.1", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-ia32/-/sass-embedded-linux-ia32-1.86.1.tgz", - "integrity": "sha512-WHntVnCgpiJPCmTeQrn5rtl1zJdd693TwpNGAFPzKD4FILPcVBKtWutl7COL6bKe/mKTf9OW0t6GBJ6mav2hAA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/sass-embedded-linux-musl-arm": { - "version": "1.86.1", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.86.1.tgz", - "integrity": "sha512-DlPpyp3bIL8YMtxR22hkWBtuZY6ch3KAmQvqIONippPv96WTHi1iq5jclbE1YXpDtI8Wcus0x6apoDSKq8o95g==", + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm/-/sass-embedded-linux-musl-arm-1.98.0.tgz", + "integrity": "sha512-OBkjTDPYR4hSaueOGIM6FDpl9nt/VZwbSRpbNu9/eEJcxE8G/vynRugW8KRZmCFjPy8j/jkGBvvS+k9iOqKV3g==", "cpu": [ "arm" ], @@ -10663,9 +10558,9 @@ } }, "node_modules/sass-embedded-linux-musl-arm64": { - "version": "1.86.1", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.86.1.tgz", - "integrity": "sha512-CwuHMRWSJFByHpgqcVtCSt29dMWhr0lpUTjaBCh9xOl0Oyz89dIqOxA0aMq+XU+thaDtOziJtMIfW6l35ZeykQ==", + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-arm64/-/sass-embedded-linux-musl-arm64-1.98.0.tgz", + "integrity": "sha512-LeqNxQA8y4opjhe68CcFvMzCSrBuJqYVFbwElEj9bagHXQHTp9xVPJRn6VcrC+0VLEDq13HVXMv7RslIuU0zmA==", "cpu": [ "arm64" ], @@ -10679,27 +10574,10 @@ "node": ">=14.0.0" } }, - "node_modules/sass-embedded-linux-musl-ia32": { - "version": "1.86.1", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-ia32/-/sass-embedded-linux-musl-ia32-1.86.1.tgz", - "integrity": "sha512-yjvVpAW1YS0VQNnIUtZTf0IrRDMa0wRjFWUtsLthVIxuXyjLy44+YULlfduxqcZe3rvI4+EqT7GorvviWo9NfQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=14.0.0" - } - }, "node_modules/sass-embedded-linux-musl-riscv64": { - "version": "1.86.1", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.86.1.tgz", - "integrity": "sha512-0zCUOMwX/hwPV1zimxM46dq/MdATSqbw6G646DwQ3/2V2Db1t9lfXBZqSavx8p/cqRp1JYTUPbJQV1gT4J7NYw==", + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-riscv64/-/sass-embedded-linux-musl-riscv64-1.98.0.tgz", + "integrity": "sha512-7w6hSuOHKt8FZsmjRb3iGSxEzM87fO9+M8nt5JIQYMhHTj5C+JY/vcske0v715HCVj5e1xyTnbGXf8FcASeAIw==", "cpu": [ "riscv64" ], @@ -10714,9 +10592,9 @@ } }, "node_modules/sass-embedded-linux-musl-x64": { - "version": "1.86.1", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.86.1.tgz", - "integrity": "sha512-8KJ6kEj1N16V9E0g5PDSd4aVe1LwcVKROJcVqnzTKPMa/4j2VuNWep7D81OYchdQMm9Egn1RqV0jCwm0b2aSHQ==", + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.98.0.tgz", + "integrity": "sha512-QikNyDEJOVqPmxyCFkci8ZdCwEssdItfjQFJB+D+Uy5HFqcS5Lv3d3GxWNX/h1dSb23RPyQdQc267ok5SbEyJw==", "cpu": [ "x64" ], @@ -10731,9 +10609,9 @@ } }, "node_modules/sass-embedded-linux-riscv64": { - "version": "1.86.1", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.86.1.tgz", - "integrity": "sha512-rNJ1EfIkQpvBfMS1fBdyb+Gsji4yK0AwsV1T7NEcy21yDxDt7mdCgkAJiaN9qf7UEXuCuueQoed7WZoDaSpjww==", + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-riscv64/-/sass-embedded-linux-riscv64-1.98.0.tgz", + "integrity": "sha512-E7fNytc/v4xFBQKzgzBddV/jretA4ULAPO6XmtBiQu4zZBdBozuSxsQLe2+XXeb0X4S2GIl72V7IPABdqke/vA==", "cpu": [ "riscv64" ], @@ -10748,9 +10626,9 @@ } }, "node_modules/sass-embedded-linux-x64": { - "version": "1.86.1", - "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.86.1.tgz", - "integrity": "sha512-DGCdUoYRRUKzRZz/q7plbB5Nean2+Uk4CqKF4RWAU0v1tHnDKKWmYfETryhWdB2WJM8QSn7O8qRebe6FCobB5g==", + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.98.0.tgz", + "integrity": "sha512-VsvP0t/uw00mMNPv3vwyYKUrFbqzxQHnRMO+bHdAMjvLw4NFf6mscpym9Bzf+NXwi1ZNKnB6DtXjmcpcvqFqYg==", "cpu": [ "x64" ], @@ -10764,29 +10642,29 @@ "node": ">=14.0.0" } }, - "node_modules/sass-embedded-win32-arm64": { - "version": "1.86.1", - "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.86.1.tgz", - "integrity": "sha512-qRLZR3yLuk/3y64YhcltkwGclhPoK6EdiLP1e5SVw5+kughcs+mNUZ3rdvSAmCSA4vDv+XOiOjRpjxmpeon95Q==", - "cpu": [ - "arm64" - ], + "node_modules/sass-embedded-unknown-all": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-unknown-all/-/sass-embedded-unknown-all-1.98.0.tgz", + "integrity": "sha512-C4MMzcAo3oEDQnW7L8SBgB9F2Fq5qHPnaYTZRMOH3Mp/7kM4OooBInXpCiiFjLnjY95hzP4KyctVx0uYR6MYlQ==", "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "!android", + "!darwin", + "!linux", + "!win32" ], - "engines": { - "node": ">=14.0.0" + "dependencies": { + "sass": "1.98.0" } }, - "node_modules/sass-embedded-win32-ia32": { - "version": "1.86.1", - "resolved": "https://registry.npmjs.org/sass-embedded-win32-ia32/-/sass-embedded-win32-ia32-1.86.1.tgz", - "integrity": "sha512-o860a7/YGHZnGeY3l/e6yt3+ZMeDdDHmthTaKnw2wpJNEq0nmytYLTJQmjWPxEMz7O8AQ0LtcbDDrhivSog+KQ==", + "node_modules/sass-embedded-win32-arm64": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-arm64/-/sass-embedded-win32-arm64-1.98.0.tgz", + "integrity": "sha512-nP/10xbAiPbhQkMr3zQfXE4TuOxPzWRQe1Hgbi90jv2R4TbzbqQTuZVOaJf7KOAN4L2Bo6XCTRjK5XkVnwZuwQ==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "license": "MIT", @@ -10799,9 +10677,9 @@ } }, "node_modules/sass-embedded-win32-x64": { - "version": "1.86.1", - "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.86.1.tgz", - "integrity": "sha512-7Z3wsVKfseJodmv689dDEV/JrXJH5TAclWNvHrEYW5BtoViOTU2pIDxRgLYzdKU9teIw5g6R0nJZb9M105oIKA==", + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass-embedded-win32-x64/-/sass-embedded-win32-x64-1.98.0.tgz", + "integrity": "sha512-/lbrVsfbcbdZQ5SJCWcV0NVPd6YRs+FtAnfedp4WbCkO/ZO7Zt/58MvI4X2BVpRY/Nt5ZBo1/7v2gYcQ+J4svQ==", "cpu": [ "x64" ], @@ -10815,13 +10693,6 @@ "node": ">=14.0.0" } }, - "node_modules/sass-embedded/node_modules/immutable": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.1.tgz", - "integrity": "sha512-3jatXi9ObIsPGr3N5hGw/vWWcTkq6hUYhpQz4k0wLC+owqWi/LiugIw9x0EdNZ2yGedKN/HzePiBvaJRXa0Ujg==", - "dev": true, - "license": "MIT" - }, "node_modules/sass-embedded/node_modules/supports-color": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", @@ -10866,13 +10737,13 @@ } }, "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-7.0.4.tgz", + "integrity": "sha512-DuGdB+Po43Q5Jxwpzt1lhyFSYKryqoNjQSA9M92tyw0lyHIOur+XCalOUe0KTJpyqzT8+fQ5A0Jf7vCx/NKmIg==", "dev": true, "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" + "engines": { + "node": ">=20.0.0" } }, "node_modules/set-cookie-parser": { @@ -11099,12 +10970,6 @@ "dev": true, "license": "ISC" }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "dev": true - }, "node_modules/simple-swizzle": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", @@ -11123,9 +10988,9 @@ "license": "MIT" }, "node_modules/sirv": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.1.tgz", - "integrity": "sha512-FoqMu0NCGBLCcAkS1qA+XJIQTR6/JHfQXl+uGteNCQ76T91DMUjPa9xfmeqMY3z80nLSg9yQmNjK0Px6RWsH/A==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-3.0.2.tgz", + "integrity": "sha512-2wcC/oGxHis/BoHkkPwldgiPSYcpZK3JU28WoMVv55yHJgcZ8rlXvuG9iZggz+sU1d4bRgIGASwyWqjxu3FM0g==", "dev": true, "license": "MIT", "dependencies": { @@ -11142,31 +11007,59 @@ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/smob": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/smob/-/smob-1.5.0.tgz", - "integrity": "sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/smob/-/smob-1.6.1.tgz", + "integrity": "sha512-KAkBqZl3c2GvNgNhcoyJae1aKldDW0LO279wF9bk1PnluRTETKBq0WyzRXxEhoQLk56yHaOY4JCBEKDuJIET5g==", "dev": true, - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=20.0.0" + } }, "node_modules/socket.io-client": { - "version": "4.7.5", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", - "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", + "version": "4.8.3", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz", + "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==", + "license": "MIT", "dependencies": { "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.5.2", + "debug": "~4.4.1", + "engine.io-client": "~6.6.1", "socket.io-parser": "~4.2.4" }, "engines": { "node": ">=10.0.0" } }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-client/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -11183,6 +11076,7 @@ "version": "0.8.0-beta.0", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.8.0-beta.0.tgz", "integrity": "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==", + "deprecated": "The work that was done in this beta branch won't be included in future versions", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -11265,6 +11159,7 @@ "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "dev": true, + "license": "MIT", "dependencies": { "escape-string-regexp": "^2.0.0" }, @@ -11277,6 +11172,7 @@ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -11288,9 +11184,9 @@ "dev": true }, "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", "dev": true, "license": "MIT", "engines": { @@ -11298,12 +11194,26 @@ } }, "node_modules/std-env": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", - "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", "dev": true, "license": "MIT" }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/strict-event-emitter": { "version": "0.5.1", "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", @@ -11326,22 +11236,6 @@ "node": ">=8" } }, - "node_modules/string-width-cjs": { - "name": "string-width", - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -11456,20 +11350,6 @@ "node": ">=8" } }, - "node_modules/strip-ansi-cjs": { - "name": "strip-ansi", - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/strip-comments": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-comments/-/strip-comments-2.0.1.tgz", @@ -11480,15 +11360,6 @@ "node": ">=10" } }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/strip-indent": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", @@ -11501,38 +11372,6 @@ "node": ">=8" } }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/strip-literal": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", - "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", - "dev": true, - "license": "MIT", - "dependencies": { - "js-tokens": "^9.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", - "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true, - "license": "MIT" - }, "node_modules/sugarss": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-5.0.1.tgz", @@ -11561,6 +11400,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -11616,6 +11456,19 @@ "integrity": "sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ==", "license": "MIT" }, + "node_modules/tagged-tag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tagged-tag/-/tagged-tag-1.0.0.tgz", + "integrity": "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/temp-dir": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", @@ -11684,74 +11537,6 @@ "dev": true, "license": "MIT" }, - "node_modules/test-exclude": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", - "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", - "dev": true, - "license": "ISC", - "dependencies": { - "@istanbuljs/schema": "^0.1.2", - "glob": "^10.4.1", - "minimatch": "^9.0.4" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", - "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", - "dev": true, - "license": "ISC", - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true - }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -11820,30 +11605,10 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, "node_modules/tinyrainbow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", - "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/tinyspy": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", - "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { @@ -11851,22 +11616,22 @@ } }, "node_modules/tldts": { - "version": "6.1.72", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.72.tgz", - "integrity": "sha512-QNtgIqSUb9o2CoUjX9T5TwaIvUUJFU1+12PJkgt42DFV2yf9J6549yTF2uGloQsJ/JOC8X+gIB81ind97hRiIQ==", + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.27.tgz", + "integrity": "sha512-I4FZcVFcqCRuT0ph6dCDpPuO4Xgzvh+spkcTr1gK7peIvxWauoloVO0vuy1FQnijT63ss6AsHB6+OIM4aXHbPg==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^6.1.72" + "tldts-core": "^7.0.27" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "6.1.72", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.72.tgz", - "integrity": "sha512-FW3H9aCaGTJ8l8RVCR3EX8GxsxDbQXuwetwwgXA2chYdsX+NY1ytCBl61narjjehWmCw92tc1AxlcY3668CU8g==", + "version": "7.0.27", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.27.tgz", + "integrity": "sha512-YQ7uPjgWUibIK6DW5lrKujGwUKhLevU4hcGbP5O6TcIUb+oTjJYJVWPS4nZsIHrEEEG6myk/oqAJUEQmpZrHsg==", "dev": true, "license": "MIT" }, @@ -11900,41 +11665,42 @@ } }, "node_modules/tough-cookie": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.0.tgz", - "integrity": "sha512-rvZUv+7MoBYTiDmFPBrhL7Ujx9Sk+q9wwm22x8c8T5IJaR+Wsyc7TNxbVxo84kZoRJZZMazowFLqpankBEQrGg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { - "tldts": "^6.1.32" + "tldts": "^7.0.5" }, "engines": { "node": ">=16" } }, "node_modules/tr46": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.0.0.tgz", - "integrity": "sha512-tk2G5R2KRwBd+ZN0zaEXpmzdKyOYksXwywulIX95MBODjSzMIuQnQ3m8JxgbhnL1LeVo7lqQKsYa1O3Htl7K5g==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", "dev": true, + "license": "MIT", "dependencies": { "punycode": "^2.3.1" }, "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/ts-api-utils": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", - "integrity": "sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", "dev": true, "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18.12" }, "peerDependencies": { - "typescript": ">=4.2.0" + "typescript": ">=4.8.4" } }, "node_modules/tslib": { @@ -11943,33 +11709,6 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/tsutils": { - "version": "3.21.0", - "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", - "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", - "dev": true, - "dependencies": { - "tslib": "^1.8.1" - }, - "engines": { - "node": ">= 6" - }, - "peerDependencies": { - "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" - } - }, - "node_modules/tsutils/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, - "node_modules/turbo-stream": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/turbo-stream/-/turbo-stream-2.4.0.tgz", - "integrity": "sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==", - "license": "ISC" - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -12073,10 +11812,11 @@ } }, "node_modules/typescript": { - "version": "5.4.4", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.4.tgz", - "integrity": "sha512-dGE2Vv8cpVvw28v8HCPqyb08EzbBURxDpuhJvTrusShUfGnhHBafDsLdS1EhhxyL6BJQE+2cT3dDPAv+MQ6oLw==", + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -12085,6 +11825,30 @@ "node": ">=14.17" } }, + "node_modules/typescript-eslint": { + "version": "8.57.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.2.tgz", + "integrity": "sha512-VEPQ0iPgWO/sBaZOU1xo4nuNdODVOajPnTIbog2GKYr31nIlZ0fWPoCQgGfF3ETyBl1vn63F/p50Um9Z4J8O8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.57.2", + "@typescript-eslint/parser": "8.57.2", + "@typescript-eslint/typescript-estree": "8.57.2", + "@typescript-eslint/utils": "8.57.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, "node_modules/unbox-primitive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", @@ -12120,10 +11884,20 @@ "url": "https://github.com/sponsors/antfu" } }, + "node_modules/undici": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.6.tgz", + "integrity": "sha512-Xi4agocCbRzt0yYMZGMA6ApD7gvtUFaxm4ZmeacWI4cZxaF6C+8I8QfofC20NAePiB/IcvZmzkJ7XPa471AEtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { - "version": "6.21.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", - "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", "dev": true, "license": "MIT" }, @@ -12152,9 +11926,9 @@ } }, "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", - "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", "dev": true, "license": "MIT", "engines": { @@ -12162,9 +11936,9 @@ } }, "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", - "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", "dev": true, "license": "MIT", "engines": { @@ -12197,14 +11971,14 @@ "node": ">=8" } }, - "node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "node_modules/until-async": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/until-async/-/until-async-3.0.2.tgz", + "integrity": "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==", "dev": true, "license": "MIT", - "engines": { - "node": ">= 4.0.0" + "funding": { + "url": "https://github.com/sponsors/kettanaito" } }, "node_modules/upath": { @@ -12219,9 +11993,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "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": [ { @@ -12254,21 +12028,11 @@ "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "punycode": "^2.1.0" } }, - "node_modules/url-parse": { - "version": "1.5.10", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", - "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, "node_modules/use-callback-ref": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", @@ -12357,6 +12121,16 @@ } } }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -12372,10 +12146,11 @@ "license": "MIT" }, "node_modules/victory-vendor": { - "version": "36.9.2", - "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", - "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "version": "37.3.6", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-37.3.6.tgz", + "integrity": "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==", "dev": true, + "license": "MIT AND ISC", "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", @@ -12394,13 +12169,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", @@ -12438,88 +12213,40 @@ }, "jiti": { "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite-node/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, - "node_modules/vite-node/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/vite-plugin-checker": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.10.3.tgz", - "integrity": "sha512-f4sekUcDPF+T+GdbbE8idb1i2YplBAoH+SfRS0e/WRBWb2rYb1Jf5Pimll0Rj+3JgIYWwG2K5LtBPCXxoibkLg==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/vite-plugin-checker/-/vite-plugin-checker-0.12.0.tgz", + "integrity": "sha512-CmdZdDOGss7kdQwv73UyVgLPv0FVYe5czAgnmRX2oKljgEvSrODGuClaV3PDR2+3ou7N/OKGauDDBjy2MB07Rg==", "dev": true, "license": "MIT", "dependencies": { @@ -12528,22 +12255,22 @@ "npm-run-path": "^6.0.0", "picocolors": "^1.1.1", "picomatch": "^4.0.3", - "strip-ansi": "^7.1.0", "tiny-invariant": "^1.3.3", - "tinyglobby": "^0.2.14", + "tinyglobby": "^0.2.15", "vscode-uri": "^3.1.0" }, "engines": { - "node": ">=14.16" + "node": ">=16.11" }, "peerDependencies": { "@biomejs/biome": ">=1.7", - "eslint": ">=7", + "eslint": ">=9.39.1", "meow": "^13.2.0", "optionator": "^0.9.4", + "oxlint": ">=1", "stylelint": ">=16", "typescript": "*", - "vite": ">=2.0.0", + "vite": ">=5.4.21", "vls": "*", "vti": "*", "vue-tsc": "~2.2.10 || ^3.0.0" @@ -12561,6 +12288,9 @@ "optionator": { "optional": true }, + "oxlint": { + "optional": true + }, "stylelint": { "optional": true }, @@ -12578,35 +12308,6 @@ } } }, - "node_modules/vite-plugin-checker/node_modules/ansi-regex": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", - "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/vite-plugin-checker/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "license": "MIT", - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/vite-plugin-checker/node_modules/npm-run-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", @@ -12650,48 +12351,18 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/vite-plugin-checker/node_modules/readdirp": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", - "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "node_modules/vite-plugin-checker/node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, "node_modules/vite-plugin-pwa": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.0.3.tgz", - "integrity": "sha512-/OpqIpUldALGxcsEnv/ekQiQ5xHkQ53wcoN5ewX4jiIDNGs3W+eNcI1WYZeyOLmzoEjg09D7aX0O89YGjen1aw==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/vite-plugin-pwa/-/vite-plugin-pwa-1.2.0.tgz", + "integrity": "sha512-a2xld+SJshT9Lgcv8Ji4+srFJL4k/1bVbd1x06JIkvecpQkwkvCncD1+gSzcdm3s+owWLpMJerG3aN5jupJEVw==", "dev": true, "license": "MIT", "dependencies": { "debug": "^4.3.6", "pretty-bytes": "^6.1.1", "tinyglobby": "^0.2.10", - "workbox-build": "^7.3.0", - "workbox-window": "^7.3.0" + "workbox-build": "^7.4.0", + "workbox-window": "^7.4.0" }, "engines": { "node": ">=16.0.0" @@ -12702,8 +12373,8 @@ "peerDependencies": { "@vite-pwa/assets-generator": "^1.0.0", "vite": "^3.1.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0", - "workbox-build": "^7.3.0", - "workbox-window": "^7.3.0" + "workbox-build": "^7.4.0", + "workbox-window": "^7.4.0" }, "peerDependenciesMeta": { "@vite-pwa/assets-generator": { @@ -12768,65 +12439,71 @@ } }, "node_modules/vitest": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", - "chai": "^5.2.0", - "debug": "^4.4.1", - "expect-type": "^1.2.1", - "magic-string": "^0.30.17", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", + "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.2", + "@vitest/mocker": "4.1.2", + "@vitest/pretty-format": "4.1.2", + "@vitest/runner": "4.1.2", + "@vitest/snapshot": "4.1.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", "pathe": "^2.0.3", - "picomatch": "^4.0.2", - "std-env": "^3.9.0", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", "tinybench": "^2.9.0", - "tinyexec": "^0.3.2", - "tinyglobby": "^0.2.14", - "tinypool": "^1.1.1", - "tinyrainbow": "^2.0.0", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", - "vite-node": "3.2.4", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" }, "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/debug": "^4.1.12", - "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.2", + "@vitest/browser-preview": "4.1.2", + "@vitest/browser-webdriverio": "4.1.2", + "@vitest/ui": "4.1.2", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, - "@types/debug": { + "@opentelemetry/api": { "optional": true }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { "optional": true }, "@vitest/ui": { @@ -12837,34 +12514,12 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, - "node_modules/vitest/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/vitest/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, "node_modules/vitest/node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", @@ -12878,6 +12533,16 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/vscode-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", @@ -12898,47 +12563,38 @@ } }, "node_modules/webidl-conversions": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", - "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", - "dev": true, - "engines": { - "node": ">=12" - } - }, - "node_modules/whatwg-encoding": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", - "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", "dev": true, - "dependencies": { - "iconv-lite": "0.6.3" - }, + "license": "BSD-2-Clause", "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/whatwg-mimetype": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", - "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=18" + "node": ">=20" } }, "node_modules/whatwg-url": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.1.0.tgz", - "integrity": "sha512-jlf/foYIKywAt3x/XWKZ/3rz8OSJPiWktjmk891alJUEjiVxKX9LEO92qH3hv4aJ0mN3MWPvGMCy8jQi95xK4w==", + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", "dev": true, "license": "MIT", "dependencies": { - "tr46": "^5.0.0", - "webidl-conversions": "^7.0.0" + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" }, "engines": { - "node": ">=18" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/which": { @@ -13024,9 +12680,9 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "version": "1.1.20", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.20.tgz", + "integrity": "sha512-LYfpUkmqwl0h9A2HL09Mms427Q1RZWuOHsukfVcKRq9q95iQxdw0ix1JQrqbcDR9PH1QDwf5Qo8OZb5lksZ8Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -13073,30 +12729,30 @@ } }, "node_modules/workbox-background-sync": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.3.0.tgz", - "integrity": "sha512-PCSk3eK7Mxeuyatb22pcSx9dlgWNv3+M8PqPaYDokks8Y5/FX4soaOqj3yhAZr5k6Q5JWTOMYgaJBpbw11G9Eg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-background-sync/-/workbox-background-sync-7.4.0.tgz", + "integrity": "sha512-8CB9OxKAgKZKyNMwfGZ1XESx89GryWTfI+V5yEj8sHjFH8MFelUwYXEyldEK6M6oKMmn807GoJFUEA1sC4XS9w==", "dev": true, "license": "MIT", "dependencies": { "idb": "^7.0.1", - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" } }, "node_modules/workbox-broadcast-update": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.3.0.tgz", - "integrity": "sha512-T9/F5VEdJVhwmrIAE+E/kq5at2OY6+OXXgOWQevnubal6sO92Gjo24v6dCVwQiclAF5NS3hlmsifRrpQzZCdUA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-broadcast-update/-/workbox-broadcast-update-7.4.0.tgz", + "integrity": "sha512-+eZQwoktlvo62cI0b+QBr40v5XjighxPq3Fzo9AWMiAosmpG5gxRHgTbGGhaJv/q/MFVxwFNGh/UwHZ/8K88lA==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" } }, "node_modules/workbox-build": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.3.0.tgz", - "integrity": "sha512-JGL6vZTPlxnlqZRhR/K/msqg3wKP+m0wfEUVosK7gsYzSgeIxvZLi1ViJJzVL7CEeI8r7rGFV973RiEqkP3lWQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-build/-/workbox-build-7.4.0.tgz", + "integrity": "sha512-Ntk1pWb0caOFIvwz/hfgrov/OJ45wPEhI5PbTywQcYjyZiVhT3UrwwUPl6TRYbTm4moaFYithYnl1lvZ8UjxcA==", "dev": true, "license": "MIT", "dependencies": { @@ -13113,33 +12769,33 @@ "common-tags": "^1.8.0", "fast-json-stable-stringify": "^2.1.0", "fs-extra": "^9.0.1", - "glob": "^7.1.6", + "glob": "^11.0.1", "lodash": "^4.17.20", "pretty-bytes": "^5.3.0", - "rollup": "^2.43.1", + "rollup": "^2.79.2", "source-map": "^0.8.0-beta.0", "stringify-object": "^3.3.0", "strip-comments": "^2.0.1", "tempy": "^0.6.0", "upath": "^1.2.0", - "workbox-background-sync": "7.3.0", - "workbox-broadcast-update": "7.3.0", - "workbox-cacheable-response": "7.3.0", - "workbox-core": "7.3.0", - "workbox-expiration": "7.3.0", - "workbox-google-analytics": "7.3.0", - "workbox-navigation-preload": "7.3.0", - "workbox-precaching": "7.3.0", - "workbox-range-requests": "7.3.0", - "workbox-recipes": "7.3.0", - "workbox-routing": "7.3.0", - "workbox-strategies": "7.3.0", - "workbox-streams": "7.3.0", - "workbox-sw": "7.3.0", - "workbox-window": "7.3.0" + "workbox-background-sync": "7.4.0", + "workbox-broadcast-update": "7.4.0", + "workbox-cacheable-response": "7.4.0", + "workbox-core": "7.4.0", + "workbox-expiration": "7.4.0", + "workbox-google-analytics": "7.4.0", + "workbox-navigation-preload": "7.4.0", + "workbox-precaching": "7.4.0", + "workbox-range-requests": "7.4.0", + "workbox-recipes": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0", + "workbox-streams": "7.4.0", + "workbox-sw": "7.4.0", + "workbox-window": "7.4.0" }, "engines": { - "node": ">=16.0.0" + "node": ">=20.0.0" } }, "node_modules/workbox-build/node_modules/@apideck/better-ajv-errors": { @@ -13160,6 +12816,16 @@ "ajv": ">=8" } }, + "node_modules/workbox-build/node_modules/@isaacs/cliui": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-9.0.0.tgz", + "integrity": "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/workbox-build/node_modules/@rollup/plugin-babel": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz", @@ -13224,9 +12890,9 @@ "license": "MIT" }, "node_modules/workbox-build/node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, "license": "MIT", "dependencies": { @@ -13247,20 +12913,45 @@ "dev": true, "license": "MIT" }, - "node_modules/workbox-build/node_modules/fs-extra": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", - "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "node_modules/workbox-build/node_modules/glob": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, - "license": "MIT", + "license": "BlueOak-1.0.0", "dependencies": { - "at-least-node": "^1.0.0", - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" + "foreground-child": "^3.3.1", + "jackspeak": "^4.1.1", + "minimatch": "^10.1.1", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "engines": { - "node": ">=10" + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/workbox-build/node_modules/jackspeak": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.2.3.tgz", + "integrity": "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^9.0.0" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/workbox-build/node_modules/json-schema-traverse": { @@ -13270,6 +12961,16 @@ "dev": true, "license": "MIT" }, + "node_modules/workbox-build/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, "node_modules/workbox-build/node_modules/magic-string": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", @@ -13280,6 +12981,23 @@ "sourcemap-codec": "^1.4.8" } }, + "node_modules/workbox-build/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/workbox-build/node_modules/pretty-bytes": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", @@ -13294,9 +13012,9 @@ } }, "node_modules/workbox-build/node_modules/rollup": { - "version": "2.79.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.79.2.tgz", - "integrity": "sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ==", + "version": "2.80.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz", + "integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==", "dev": true, "license": "MIT", "bin": { @@ -13309,160 +13027,143 @@ "fsevents": "~2.3.2" } }, - "node_modules/workbox-build/node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/workbox-cacheable-response": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.3.0.tgz", - "integrity": "sha512-eAFERIg6J2LuyELhLlmeRcJFa5e16Mj8kL2yCDbhWE+HUun9skRQrGIFVUagqWj4DMaaPSMWfAolM7XZZxNmxA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-cacheable-response/-/workbox-cacheable-response-7.4.0.tgz", + "integrity": "sha512-0Fb8795zg/x23ISFkAc7lbWes6vbw34DGFIMw31cwuHPgDEC/5EYm6m/ZkylLX0EnEbbOyOCLjKgFS/Z5g0HeQ==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" } }, "node_modules/workbox-core": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.3.0.tgz", - "integrity": "sha512-Z+mYrErfh4t3zi7NVTvOuACB0A/jA3bgxUN3PwtAVHvfEsZxV9Iju580VEETug3zYJRc0Dmii/aixI/Uxj8fmw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.4.0.tgz", + "integrity": "sha512-6BMfd8tYEnN4baG4emG9U0hdXM4gGuDU3ectXuVHnj71vwxTFI7WOpQJC4siTOlVtGqCUtj0ZQNsrvi6kZZTAQ==", "dev": true, "license": "MIT" }, "node_modules/workbox-expiration": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.3.0.tgz", - "integrity": "sha512-lpnSSLp2BM+K6bgFCWc5bS1LR5pAwDWbcKt1iL87/eTSJRdLdAwGQznZE+1czLgn/X05YChsrEegTNxjM067vQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-expiration/-/workbox-expiration-7.4.0.tgz", + "integrity": "sha512-V50p4BxYhtA80eOvulu8xVfPBgZbkxJ1Jr8UUn0rvqjGhLDqKNtfrDfjJKnLz2U8fO2xGQJTx/SKXNTzHOjnHw==", "dev": true, "license": "MIT", "dependencies": { "idb": "^7.0.1", - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" } }, "node_modules/workbox-google-analytics": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.3.0.tgz", - "integrity": "sha512-ii/tSfFdhjLHZ2BrYgFNTrb/yk04pw2hasgbM70jpZfLk0vdJAXgaiMAWsoE+wfJDNWoZmBYY0hMVI0v5wWDbg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-google-analytics/-/workbox-google-analytics-7.4.0.tgz", + "integrity": "sha512-MVPXQslRF6YHkzGoFw1A4GIB8GrKym/A5+jYDUSL+AeJw4ytQGrozYdiZqUW1TPQHW8isBCBtyFJergUXyNoWQ==", "dev": true, "license": "MIT", "dependencies": { - "workbox-background-sync": "7.3.0", - "workbox-core": "7.3.0", - "workbox-routing": "7.3.0", - "workbox-strategies": "7.3.0" + "workbox-background-sync": "7.4.0", + "workbox-core": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0" } }, "node_modules/workbox-navigation-preload": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.3.0.tgz", - "integrity": "sha512-fTJzogmFaTv4bShZ6aA7Bfj4Cewaq5rp30qcxl2iYM45YD79rKIhvzNHiFj1P+u5ZZldroqhASXwwoyusnr2cg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-navigation-preload/-/workbox-navigation-preload-7.4.0.tgz", + "integrity": "sha512-etzftSgdQfjMcfPgbfaZCfM2QuR1P+4o8uCA2s4rf3chtKTq/Om7g/qvEOcZkG6v7JZOSOxVYQiOu6PbAZgU6w==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" } }, "node_modules/workbox-precaching": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.3.0.tgz", - "integrity": "sha512-ckp/3t0msgXclVAYaNndAGeAoWQUv7Rwc4fdhWL69CCAb2UHo3Cef0KIUctqfQj1p8h6aGyz3w8Cy3Ihq9OmIw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-precaching/-/workbox-precaching-7.4.0.tgz", + "integrity": "sha512-VQs37T6jDqf1rTxUJZXRl3yjZMf5JX/vDPhmx2CPgDDKXATzEoqyRqhYnRoxl6Kr0rqaQlp32i9rtG5zTzIlNg==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "7.3.0", - "workbox-routing": "7.3.0", - "workbox-strategies": "7.3.0" + "workbox-core": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0" } }, "node_modules/workbox-range-requests": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.3.0.tgz", - "integrity": "sha512-EyFmM1KpDzzAouNF3+EWa15yDEenwxoeXu9bgxOEYnFfCxns7eAxA9WSSaVd8kujFFt3eIbShNqa4hLQNFvmVQ==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-range-requests/-/workbox-range-requests-7.4.0.tgz", + "integrity": "sha512-3Vq854ZNuP6Y0KZOQWLaLC9FfM7ZaE+iuQl4VhADXybwzr4z/sMmnLgTeUZLq5PaDlcJBxYXQ3U91V7dwAIfvw==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" } }, "node_modules/workbox-recipes": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.3.0.tgz", - "integrity": "sha512-BJro/MpuW35I/zjZQBcoxsctgeB+kyb2JAP5EB3EYzePg8wDGoQuUdyYQS+CheTb+GhqJeWmVs3QxLI8EBP1sg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-recipes/-/workbox-recipes-7.4.0.tgz", + "integrity": "sha512-kOkWvsAn4H8GvAkwfJTbwINdv4voFoiE9hbezgB1sb/0NLyTG4rE7l6LvS8lLk5QIRIto+DjXLuAuG3Vmt3cxQ==", "dev": true, "license": "MIT", "dependencies": { - "workbox-cacheable-response": "7.3.0", - "workbox-core": "7.3.0", - "workbox-expiration": "7.3.0", - "workbox-precaching": "7.3.0", - "workbox-routing": "7.3.0", - "workbox-strategies": "7.3.0" + "workbox-cacheable-response": "7.4.0", + "workbox-core": "7.4.0", + "workbox-expiration": "7.4.0", + "workbox-precaching": "7.4.0", + "workbox-routing": "7.4.0", + "workbox-strategies": "7.4.0" } }, "node_modules/workbox-routing": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.3.0.tgz", - "integrity": "sha512-ZUlysUVn5ZUzMOmQN3bqu+gK98vNfgX/gSTZ127izJg/pMMy4LryAthnYtjuqcjkN4HEAx1mdgxNiKJMZQM76A==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-routing/-/workbox-routing-7.4.0.tgz", + "integrity": "sha512-C/ooj5uBWYAhAqwmU8HYQJdOjjDKBp9MzTQ+otpMmd+q0eF59K+NuXUek34wbL0RFrIXe/KKT+tUWcZcBqxbHQ==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" } }, "node_modules/workbox-strategies": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.3.0.tgz", - "integrity": "sha512-tmZydug+qzDFATwX7QiEL5Hdf7FrkhjaF9db1CbB39sDmEZJg3l9ayDvPxy8Y18C3Y66Nrr9kkN1f/RlkDgllg==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-strategies/-/workbox-strategies-7.4.0.tgz", + "integrity": "sha512-T4hVqIi5A4mHi92+5EppMX3cLaVywDp8nsyUgJhOZxcfSV/eQofcOA6/EMo5rnTNmNTpw0rUgjAI6LaVullPpg==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "7.3.0" + "workbox-core": "7.4.0" } }, "node_modules/workbox-streams": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.3.0.tgz", - "integrity": "sha512-SZnXucyg8x2Y61VGtDjKPO5EgPUG5NDn/v86WYHX+9ZqvAsGOytP0Jxp1bl663YUuMoXSAtsGLL+byHzEuMRpw==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-streams/-/workbox-streams-7.4.0.tgz", + "integrity": "sha512-QHPBQrey7hQbnTs5GrEVoWz7RhHJXnPT+12qqWM378orDMo5VMJLCkCM1cnCk+8Eq92lccx/VgRZ7WAzZWbSLg==", "dev": true, "license": "MIT", "dependencies": { - "workbox-core": "7.3.0", - "workbox-routing": "7.3.0" + "workbox-core": "7.4.0", + "workbox-routing": "7.4.0" } }, "node_modules/workbox-sw": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.3.0.tgz", - "integrity": "sha512-aCUyoAZU9IZtH05mn0ACUpyHzPs0lMeJimAYkQkBsOWiqaJLgusfDCR+yllkPkFRxWpZKF8vSvgHYeG7LwhlmA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-sw/-/workbox-sw-7.4.0.tgz", + "integrity": "sha512-ltU+Kr3qWR6BtbdlMnCjobZKzeV1hN+S6UvDywBrwM19TTyqA03X66dzw1tEIdJvQ4lYKkBFox6IAEhoSEZ8Xw==", "dev": true, "license": "MIT" }, "node_modules/workbox-window": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.3.0.tgz", - "integrity": "sha512-qW8PDy16OV1UBaUNGlTVcepzrlzyzNW/ZJvFQQs2j2TzGsg6IKjcpZC1RSquqQnTOafl5pCj5bGfAHlCjOOjdA==", + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/workbox-window/-/workbox-window-7.4.0.tgz", + "integrity": "sha512-/bIYdBLAVsNR3v7gYGaV4pQW3M3kEPx5E8vDxGvxo6khTrGtSSCS7QiFKv9ogzBgZiy0OXLP9zO28U/1nF1mfw==", "dev": true, "license": "MIT", "dependencies": { "@types/trusted-types": "^2.0.2", - "workbox-core": "7.1.0" + "workbox-core": "7.4.0" } }, - "node_modules/workbox-window/node_modules/workbox-core": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/workbox-core/-/workbox-core-7.1.0.tgz", - "integrity": "sha512-5KB4KOY8rtL31nEF7BfvU7FMzKT4B5TkbYa2tzkS+Peqj0gayMT9SytSFtNzlrvMaWgv6y/yvP9C0IbpFjV30Q==", - "dev": true, - "license": "MIT" - }, "node_modules/wrap-ansi": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", @@ -13478,35 +13179,10 @@ "node": ">=8" } }, - "node_modules/wrap-ansi-cjs": { - "name": "wrap-ansi", - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, "node_modules/ws": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", - "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", "license": "MIT", "engines": { "node": ">=10.0.0" @@ -13540,9 +13216,9 @@ "dev": true }, "node_modules/xmlhttprequest-ssl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", - "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", "engines": { "node": ">=0.4.0" } @@ -13564,16 +13240,19 @@ "dev": true }, "node_modules/yaml": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz", - "integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==", + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yargs": { @@ -13629,6 +13308,29 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-validation-error": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz", + "integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + } } } } diff --git a/frontend/package.json b/frontend/package.json index 0650a45ad8..7204befda2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -4,82 +4,87 @@ "description": "Bazarr is a companion application to Sonarr and Radarr. It manages and downloads subtitles based on your requirements. You define your preferences by TV show or movie and Bazarr takes care of everything for you.", "repository": { "type": "git", - "url": "git+https://github.com/morpheus65535/bazarr.git" + "url": "git+https://github.com/LavX/bazarr.git" }, - "author": "morpheus65535", + "author": "LavX", "license": "GPL-3", "bugs": { - "url": "https://github.com/morpheus65535/bazarr/issues" + "url": "https://github.com/LavX/bazarr/issues" }, "private": true, "dependencies": { - "@mantine/core": "^8.3.9", - "@mantine/dropzone": "^8.3.9", - "@mantine/form": "^8.3.9", - "@mantine/hooks": "^8.3.9", - "@mantine/modals": "^8.3.9", - "@mantine/notifications": "^8.3.9", - "@tanstack/react-query": "^5.64.1", - "@tanstack/react-table": "^8.19.2", - "axios": "^1.8.2", + "@fontsource-variable/geist": "^5.2.8", + "@mantine/core": "^8.3.16", + "@mantine/dropzone": "^8.3.16", + "@mantine/form": "^8.3.16", + "@mantine/hooks": "^8.3.16", + "@mantine/modals": "^8.3.16", + "@mantine/notifications": "^8.3.16", + "@tanstack/react-query": "^5.90.21", + "@tanstack/react-table": "^8.21.3", + "@tanstack/react-virtual": "^3.13.23", + "axios": "^1.13.6", "braces": "^3.0.3", - "react": "^19.2.3", - "react-dom": "^19.2.3", - "react-router": "^7.1.1", + "react": "^19.2.4", + "react-dom": "^19.2.4", + "react-router": "^7.13.1", "react-timeago": "^8.3.0", - "socket.io-client": "^4.7.5" + "socket.io-client": "^4.8.3" }, "devDependencies": { - "@fontsource/roboto": "^5.0.12", - "@fortawesome/fontawesome-svg-core": "^7.1.0", - "@fortawesome/free-brands-svg-icons": "^7.1.0", - "@fortawesome/free-regular-svg-icons": "^7.1.0", - "@fortawesome/free-solid-svg-icons": "^7.1.0", - "@fortawesome/react-fontawesome": "^3.1.0", - "@tanstack/react-query-devtools": "^5.40.1", - "@testing-library/jest-dom": "^6.4.2", - "@testing-library/react": "^16.1.0", - "@testing-library/user-event": "^14.5.2", - "@types/jest": "^29.5.12", - "@types/lodash": "^4.17.1", - "@types/node": "^22.14.1", - "@types/react": "^19.2.3", + "@eslint/js": "^10.0.1", + "@fortawesome/fontawesome-svg-core": "^7.2.0", + "@fortawesome/free-brands-svg-icons": "^7.2.0", + "@fortawesome/free-regular-svg-icons": "^7.2.0", + "@fortawesome/free-solid-svg-icons": "^7.2.0", + "@fortawesome/react-fontawesome": "^3.2.0", + "@tanstack/react-query-devtools": "^5.91.3", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@testing-library/user-event": "^14.6.1", + "@types/jest": "^30.0.0", + "@types/lodash": "^4.17.24", + "@types/node": "^25.4.0", + "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", - "@typescript-eslint/eslint-plugin": "^7.16.0", - "@typescript-eslint/parser": "^7.16.0", - "@vite-pwa/assets-generator": "^1.0.0", - "@vitejs/plugin-react": "^5.0.1", - "@vitest/coverage-v8": "^3.2.4", - "@vitest/ui": "^3.2.4", - "clsx": "^2.1.0", - "eslint": "^8.57.0", - "eslint-plugin-react-hooks": "^4.6.0", - "eslint-plugin-react-refresh": "^0.4.7", - "eslint-plugin-simple-import-sort": "^12.1.0", - "eslint-plugin-testing-library": "^6.2.0", - "husky": "^9.0.11", - "jsdom": "^26.0.0", - "lodash": "^4.17.21", - "msw": "^2.7.0", + "@typescript-eslint/eslint-plugin": "^8.57.0", + "@typescript-eslint/parser": "^8.57.0", + "@vite-pwa/assets-generator": "^1.0.2", + "@vitejs/plugin-react": "^5.1.4", + "@vitest/coverage-v8": "^4.0.18", + "@vitest/ui": "^4.0.18", + "clsx": "^2.1.1", + "eslint": "^10.0.3", + "eslint-plugin-react-hooks": "^7.0.1", + "eslint-plugin-react-refresh": "^0.5.2", + "eslint-plugin-simple-import-sort": "^12.1.1", + "eslint-plugin-testing-library": "^7.16.0", + "globals": "^17.4.0", + "husky": "^9.1.7", + "jsdom": "^28.1.0", + "lodash": "^4.17.23", + "msw": "^2.12.10", "postcss-preset-mantine": "^1.18.0", "postcss-simple-vars": "^7.0.1", - "prettier": "^3.2.5", - "prettier-plugin-organize-imports": "^3.2.4", - "pretty-quick": "^4.0.0", - "recharts": "^2.15.0", - "sass-embedded": "^1.86.1", - "typescript": "^5.4.4", - "vite": "^7.1.3", - "vite-plugin-checker": "^0.10.3", - "vite-plugin-pwa": "^1.0.3", - "vitest": "^3.2.4", - "yaml": "^2.4.1" + "prettier": "^3.8.1", + "prettier-plugin-organize-imports": "^4.3.0", + "pretty-quick": "^4.2.2", + "recharts": "^3.8.0", + "sass-embedded": "^1.98.0", + "typescript": "^5.9.3", + "typescript-eslint": "^8.57.0", + "vite": "^7.3.1", + "vite-plugin-checker": "^0.12.0", + "vite-plugin-pwa": "^1.2.0", + "vitest": "^4.0.18", + "yaml": "^2.8.2" }, "scripts": { "build": "vite build", "build:ci": "vite build -m development", - "check": "eslint --ext .ts,.tsx src", - "check:fix": "eslint --ext .ts,.tsx src --fix", + "check": "eslint src", + "check:fix": "eslint src --fix", "check:ts": "tsc --noEmit --incremental false", "check:fmt": "prettier -c .", "coverage": "vitest run --coverage", @@ -102,5 +107,11 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "overrides": { + "serialize-javascript": "^7.0.4", + "eslint-plugin-react-hooks": { + "eslint": "$eslint" + } } } diff --git a/frontend/public/images/android-chrome-96x96.png b/frontend/public/images/android-chrome-96x96.png index a9227b96a8..53ca9176d2 100644 Binary files a/frontend/public/images/android-chrome-96x96.png and b/frontend/public/images/android-chrome-96x96.png differ diff --git a/frontend/public/images/apple-touch-icon-180x180.png b/frontend/public/images/apple-touch-icon-180x180.png index 01c19a6eeb..5054fb650e 100644 Binary files a/frontend/public/images/apple-touch-icon-180x180.png and b/frontend/public/images/apple-touch-icon-180x180.png differ diff --git a/frontend/public/images/favicon-16x16.png b/frontend/public/images/favicon-16x16.png index 03618a704f..f15db17621 100644 Binary files a/frontend/public/images/favicon-16x16.png and b/frontend/public/images/favicon-16x16.png differ diff --git a/frontend/public/images/favicon-32x32.png b/frontend/public/images/favicon-32x32.png index 85dbb8462e..d4f4f07a67 100644 Binary files a/frontend/public/images/favicon-32x32.png and b/frontend/public/images/favicon-32x32.png differ diff --git a/frontend/public/images/favicon.ico b/frontend/public/images/favicon.ico index 02c0a64122..149200567b 100644 Binary files a/frontend/public/images/favicon.ico and b/frontend/public/images/favicon.ico differ diff --git a/frontend/public/images/logo.png b/frontend/public/images/logo.png new file mode 100644 index 0000000000..d0d9c2d764 Binary files /dev/null and b/frontend/public/images/logo.png differ diff --git a/frontend/public/images/logo.svg b/frontend/public/images/logo.svg new file mode 100644 index 0000000000..49a19b755d --- /dev/null +++ b/frontend/public/images/logo.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/public/images/logo128.png b/frontend/public/images/logo128.png index 467bde320b..4f8fe42bdc 100644 Binary files a/frontend/public/images/logo128.png and b/frontend/public/images/logo128.png differ diff --git a/frontend/public/images/logo64.png b/frontend/public/images/logo64.png index 891190143f..03e7685768 100644 Binary files a/frontend/public/images/logo64.png and b/frontend/public/images/logo64.png differ diff --git a/frontend/public/images/logo_minimal_b.png b/frontend/public/images/logo_minimal_b.png new file mode 100644 index 0000000000..585df9c81c Binary files /dev/null and b/frontend/public/images/logo_minimal_b.png differ diff --git a/frontend/public/images/logo_minimal_b.svg b/frontend/public/images/logo_minimal_b.svg new file mode 100644 index 0000000000..2651b2727d --- /dev/null +++ b/frontend/public/images/logo_minimal_b.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/public/images/logo_minimal_b128.png b/frontend/public/images/logo_minimal_b128.png new file mode 100644 index 0000000000..8718706cba Binary files /dev/null and b/frontend/public/images/logo_minimal_b128.png differ diff --git a/frontend/public/images/logo_minimal_b64.png b/frontend/public/images/logo_minimal_b64.png new file mode 100644 index 0000000000..16c976043a Binary files /dev/null and b/frontend/public/images/logo_minimal_b64.png differ diff --git a/frontend/public/images/logo_minimal_w.png b/frontend/public/images/logo_minimal_w.png new file mode 100644 index 0000000000..db081b03cc Binary files /dev/null and b/frontend/public/images/logo_minimal_w.png differ diff --git a/frontend/public/images/logo_minimal_w.svg b/frontend/public/images/logo_minimal_w.svg new file mode 100644 index 0000000000..99436cb939 --- /dev/null +++ b/frontend/public/images/logo_minimal_w.svg @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/frontend/public/images/logo_minimal_w128.png b/frontend/public/images/logo_minimal_w128.png new file mode 100644 index 0000000000..951f259f23 Binary files /dev/null and b/frontend/public/images/logo_minimal_w128.png differ diff --git a/frontend/public/images/logo_minimal_w64.png b/frontend/public/images/logo_minimal_w64.png new file mode 100644 index 0000000000..675b926be6 Binary files /dev/null and b/frontend/public/images/logo_minimal_w64.png differ diff --git a/frontend/public/images/logo_no_orb.png b/frontend/public/images/logo_no_orb.png new file mode 100644 index 0000000000..c89c016e68 Binary files /dev/null and b/frontend/public/images/logo_no_orb.png differ diff --git a/frontend/public/images/logo_no_orb.svg b/frontend/public/images/logo_no_orb.svg new file mode 100644 index 0000000000..01ae43816d --- /dev/null +++ b/frontend/public/images/logo_no_orb.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/frontend/public/images/logo_no_orb128.png b/frontend/public/images/logo_no_orb128.png new file mode 100644 index 0000000000..e0cfc893dd Binary files /dev/null and b/frontend/public/images/logo_no_orb128.png differ diff --git a/frontend/public/images/logo_no_orb64.png b/frontend/public/images/logo_no_orb64.png new file mode 100644 index 0000000000..46804394d3 Binary files /dev/null and b/frontend/public/images/logo_no_orb64.png differ diff --git a/frontend/public/images/maskable-icon-512x512.png b/frontend/public/images/maskable-icon-512x512.png index e813d5ddf7..a6f20b650d 100644 Binary files a/frontend/public/images/maskable-icon-512x512.png and b/frontend/public/images/maskable-icon-512x512.png differ diff --git a/frontend/public/images/mstile-150x150.png b/frontend/public/images/mstile-150x150.png index 362cfe6c53..1cbdf406eb 100644 Binary files a/frontend/public/images/mstile-150x150.png and b/frontend/public/images/mstile-150x150.png differ diff --git a/frontend/public/images/pwa-192x192.png b/frontend/public/images/pwa-192x192.png index a93a96a653..dc45d26c54 100644 Binary files a/frontend/public/images/pwa-192x192.png and b/frontend/public/images/pwa-192x192.png differ diff --git a/frontend/public/images/pwa-512x512.png b/frontend/public/images/pwa-512x512.png index 298f27dbae..b05f07065d 100644 Binary files a/frontend/public/images/pwa-512x512.png and b/frontend/public/images/pwa-512x512.png differ diff --git a/frontend/public/images/pwa-64x64.png b/frontend/public/images/pwa-64x64.png index 41487e49c5..fa0f87cbd0 100644 Binary files a/frontend/public/images/pwa-64x64.png and b/frontend/public/images/pwa-64x64.png differ diff --git a/frontend/src/App/Header.module.scss b/frontend/src/App/Header.module.scss index 8f22398097..47e5096592 100644 --- a/frontend/src/App/Header.module.scss +++ b/frontend/src/App/Header.module.scss @@ -1,9 +1,52 @@ .header { - @include mantine.light { - color: var(--mantine-color-gray-0); + box-shadow: none; +} + +.headerInner { + background: var(--bz-surface-base); + border: 1px solid var(--bz-border-card); + border-radius: var(--bz-radius-xl); + padding: 10px 16px; + margin-right: 8px; + display: flex; + align-items: center; + + color: var(--bz-text-primary); + + // Ghost-style action buttons (theme toggle, notifications, system gear) + :global(.mantine-ActionIcon-root) { + width: 32px; + height: 32px; + min-width: 32px; + min-height: 32px; + background: var(--bz-hover-bg); + border: 1px solid var(--bz-border-interactive); + border-radius: var(--bz-radius-md); + color: var(--bz-text-tertiary); + font-size: 12px; + font-weight: 500; + &:hover { + background: var(--bz-hover-bg-emphasis); + } } - @include mantine.dark { - color: var(--mantine-color-dark-0); + // Search input + :global(.mantine-Select-input) { + background: var(--bz-hover-bg); + border: 1px solid var(--bz-border-interactive); + border-radius: var(--bz-radius-md); + transition: background var(--bz-duration-normal) var(--bz-ease-standard); + + &::placeholder { + color: var(--bz-text-tertiary); + } + + &:hover { + background: var(--bz-hover-bg-emphasis); + } + + &:focus { + background: var(--bz-hover-bg-emphasis); + } } } diff --git a/frontend/src/App/Header.tsx b/frontend/src/App/Header.tsx index 11cb58e8a5..89f56d3858 100644 --- a/frontend/src/App/Header.tsx +++ b/frontend/src/App/Header.tsx @@ -3,18 +3,22 @@ import { Anchor, AppShell, Avatar, - Badge, Burger, Divider, Group, Menu, + Text, + useComputedColorScheme, + useMantineColorScheme, } from "@mantine/core"; import { useDisclosure } from "@mantine/hooks"; -import { faBell } from "@fortawesome/free-regular-svg-icons/faBell"; +import { faListCheck } from "@fortawesome/free-solid-svg-icons"; import { faArrowRotateLeft, faGear, + faMoon, faPowerOff, + faSun, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { useSystem, useSystemJobs, useSystemSettings } from "@/apis/hooks"; @@ -38,6 +42,9 @@ const AppHeader: FunctionComponent = () => { const goHome = useGotoHomepage(); + const { toggleColorScheme } = useMantineColorScheme(); + const dark = useComputedColorScheme("light") === "dark"; + const [ jobsManagerOpened, { open: openJobsManager, close: closeJobsManager }, @@ -46,70 +53,104 @@ const AppHeader: FunctionComponent = () => { const { data: jobs } = useSystemJobs(); return ( - - - - show(!showed)} - size="sm" - hiddenFrom="sm" - > - - - - - Bazarr - - - - - job.status === "running").length, - )} - onClick={openJobsManager} - > - - - - - - } - onClick={() => restart()} - > - Restart - - } - onClick={() => shutdown()} - > - Shutdown - - - - - + +
+ + + show(!showed)} + size="sm" + hiddenFrom="sm" + > + + + + + Bazarr + + + + + + + + +
+ +
+ + toggleColorScheme()} + icon={dark ? faSun : faMoon} + size="sm" + > + job.status === "running").length, + )} + onClick={openJobsManager} + > + + + + + + } + onClick={() => restart()} + > + Restart + + } + onClick={() => shutdown()} + > + Shutdown + + + + + +
- +
{ - const [selection, select] = useState(null); +// Section grouping configuration. +// Routes are matched by their path property. +const sectionGroups = [ + { label: "Media", paths: ["series", "movies"] }, + { label: "Management", paths: ["history", "wanted", "blacklist"] }, + { label: "System", paths: ["settings", "system"] }, +]; + +function groupRoutes(routes: CustomRouteObject[]) { + // Filter to visible nav items (have a path, not hidden, not index-only) + const navItems = routes.filter( + (r) => r.path !== undefined && !r.hidden && !r.path.includes(":") && r.name, + ); - const { toggleColorScheme } = useMantineColorScheme(); - const computedColorScheme = useComputedColorScheme("light"); + const groups: { label: string; items: CustomRouteObject[] }[] = []; - const dark = computedColorScheme === "dark"; + for (const section of sectionGroups) { + const items = section.paths + .map((p) => navItems.find((r) => r.path === p)) + .filter((r): r is CustomRouteObject => r !== undefined); + + if (items.length > 0) { + groups.push({ label: section.label, items }); + } + } + + // Catch any remaining items not in a defined group + const groupedPaths = new Set(sectionGroups.flatMap((s) => s.paths)); + const ungrouped = navItems.filter((r) => !groupedPaths.has(r.path ?? "")); + if (ungrouped.length > 0) { + groups.push({ label: "Other", items: ungrouped }); + } + + return groups; +} + +const AppNavbar: FunctionComponent = () => { + const [selection, select] = useState(null); const routes = useRouteItems(); @@ -111,41 +125,36 @@ const AppNavbar: FunctionComponent = () => { select(null); }, [pathname]); + // The top-level route (path "/") contains the nav items as children. + // useRouteItems returns the full routes array, and the nameless "/" route + // renders its children directly. We need to find the app route's children. + const navRoutes = useMemo(() => { + const appRoute = routes.find((r) => r.path === "/"); + return appRoute?.children ?? routes; + }, [routes]); + + const groups = useMemo(() => groupRoutes(navRoutes), [navRoutes]); + return ( - - - + +
+ - {routes.map((route, idx) => ( - + {groups.map((group) => ( +
+
{group.label}
+ {group.items.map((route, idx) => ( + + ))} +
))}
- - - - - toggleColorScheme()} - icon={dark ? faSun : faMoon} - > - - - - - -
+ +
); }; @@ -177,7 +186,7 @@ const RouteItem: FunctionComponent<{ parent={link} key={BuildKey(link, "nav", idx)} route={child} - > + /> ))} ); @@ -186,7 +195,6 @@ const RouteItem: FunctionComponent<{ return ( + /> @@ -219,12 +227,7 @@ const RouteItem: FunctionComponent<{ } } else { return ( - + ); } }; @@ -234,7 +237,6 @@ interface NavbarItemProps { link: string; icon?: IconDefinition; badge?: number | string; - primary?: boolean; onClick?: (event: React.MouseEvent) => void; } @@ -244,7 +246,6 @@ const NavbarItem: FunctionComponent = ({ name, badge, onClick, - primary = false, }) => { const { show } = useNavbar(); @@ -278,24 +279,11 @@ const NavbarItem: FunctionComponent = ({ ) } > - - {icon && ( - - )} + + {icon && } {name} {!shouldHideBadge && ( - + {badge} )} diff --git a/frontend/src/App/NotificationDrawer.tsx b/frontend/src/App/NotificationDrawer.tsx index 170114609f..b4917afb40 100644 --- a/frontend/src/App/NotificationDrawer.tsx +++ b/frontend/src/App/NotificationDrawer.tsx @@ -139,7 +139,7 @@ const NotificationDrawer: FunctionComponent = ({ )} {!jobsLoading && jobsError && ( - + Failed to load jobs. @@ -227,7 +227,7 @@ const NotificationDrawer: FunctionComponent = ({ )}
- + {grouped[status as string].length} job {grouped[status as string].length > 1 ? "s" : ""} @@ -254,7 +254,7 @@ const NotificationDrawer: FunctionComponent = ({ = ({ position="right" > = ({ job.progress_max) * 100 : 0, - color: "brand", + color: + status === "completed" + ? "green" + : status === "failed" + ? "red" + : "brand", }, ]} label={ {status === "completed" && job.progress_max == 0 && @@ -467,7 +477,10 @@ const NotificationDrawer: FunctionComponent = ({
{job?.progress_message && ( - + {job.progress_message} )} @@ -481,14 +494,14 @@ const NotificationDrawer: FunctionComponent = ({ )); })() ) : ( - + No jobs to display )} ) : ( - - + + Jobs { setOnline(detail.online); }); + const [upgradeModalOpen, setUpgradeModalOpen] = useState(false); + const [upgrading, setUpgrading] = useState(false); + useEffect(() => { if (Environment.hasUpdate) { showNotification( @@ -46,6 +50,41 @@ const App: FunctionComponent = () => { } }, []); + useEffect(() => { + const token = sessionStorage.getItem("password_upgrade_token"); + if (token) { + setUpgradeModalOpen(true); + } + }, []); + + const handleUpgradeAccept = useCallback(async () => { + const token = sessionStorage.getItem("password_upgrade_token"); + if (!token) return; + setUpgrading(true); + try { + await api.system.upgradePasswordHash(token); + showNotification( + notification.info( + "Password upgraded", + "Your password hash has been upgraded to PBKDF2-SHA256", + ), + ); + } catch { + showNotification( + notification.warn("Upgrade failed", "Could not upgrade password hash"), + ); + } finally { + sessionStorage.removeItem("password_upgrade_token"); + setUpgradeModalOpen(false); + setUpgrading(false); + } + }, []); + + const handleUpgradeDecline = useCallback(() => { + sessionStorage.removeItem("password_upgrade_token"); + setUpgradeModalOpen(false); + }, []); + if (criticalError !== null) { return ; } @@ -69,6 +108,31 @@ const App: FunctionComponent = () => { + + + + Your password is currently stored using a weak MD5 hash. Would + you like to upgrade to PBKDF2-SHA256 for better security? + + + Note: After upgrading, reverting to upstream Bazarr will require + resetting your password via the config file. + + + + + + + diff --git a/frontend/src/Router/index.tsx b/frontend/src/Router/index.tsx index 7ccff2ceb9..0bdf5b37ff 100644 --- a/frontend/src/Router/index.tsx +++ b/frontend/src/Router/index.tsx @@ -28,9 +28,7 @@ import MoviesHistoryView from "@/pages/History/Movies"; import SeriesHistoryView from "@/pages/History/Series"; import MovieView from "@/pages/Movies"; import MovieDetailView from "@/pages/Movies/Details"; -import MovieMassEditor from "@/pages/Movies/Editor"; import SeriesView from "@/pages/Series"; -import SeriesMassEditor from "@/pages/Series/Editor"; import SettingsGeneralView from "@/pages/Settings/General"; import SettingsLanguagesView from "@/pages/Settings/Languages"; import SettingsNotificationsView from "@/pages/Settings/Notifications"; @@ -40,6 +38,7 @@ import SettingsRadarrView from "@/pages/Settings/Radarr"; import SettingsSchedulerView from "@/pages/Settings/Scheduler"; import SettingsSonarrView from "@/pages/Settings/Sonarr"; import SettingsSubtitlesView from "@/pages/Settings/Subtitles"; +import SettingsTranslatorView from "@/pages/Settings/Translator"; import SettingsUIView from "@/pages/Settings/UI"; import SystemAnnouncementsView from "@/pages/System/Announcements"; import SystemBackupsView from "@/pages/System/Backups"; @@ -58,6 +57,7 @@ const HistoryStats = lazy( () => import("@/pages/History/Statistics/HistoryStats"), ); const SystemStatusView = lazy(() => import("@/pages/System/Status")); +const SubtitleEditor = lazy(() => import("@/pages/SubtitleEditor")); function useRoutes(): CustomRouteObject[] { const { data } = useBadges(); @@ -84,11 +84,6 @@ function useRoutes(): CustomRouteObject[] { index: true, element: , }, - { - path: "edit", - hidden: true, - element: , - }, { path: ":id", element: , @@ -106,11 +101,6 @@ function useRoutes(): CustomRouteObject[] { index: true, element: , }, - { - path: "edit", - hidden: true, - element: , - }, { path: ":id", element: , @@ -228,6 +218,11 @@ function useRoutes(): CustomRouteObject[] { name: "Plex", element: , }, + { + path: "translator", + name: "AI Translator", + element: , + }, { path: "notifications", name: "Notifications", @@ -296,6 +291,15 @@ function useRoutes(): CustomRouteObject[] { }, ], }, + { + path: "subtitles/preview/:mediaType/:mediaId/:language", + hidden: true, + element: ( + + + + ), + }, { path: "*", hidden: true, diff --git a/frontend/src/apis/hooks/plex.ts b/frontend/src/apis/hooks/plex.ts index 497d358ea1..873aab6a25 100644 --- a/frontend/src/apis/hooks/plex.ts +++ b/frontend/src/apis/hooks/plex.ts @@ -70,6 +70,7 @@ export const usePlexPinMutation = () => { export const usePlexPinCheckQuery = ( pinId: string | null, + state: string | null, enabled: boolean, refetchInterval: number | false, ) => { @@ -77,7 +78,7 @@ export const usePlexPinCheckQuery = ( queryKey: [QueryKeys.Plex, "pinCheck", pinId], queryFn: () => { if (!pinId) throw new Error("Pin ID is required"); - return api.plex.checkPin(pinId); + return api.plex.checkPin(pinId, state ?? undefined); }, enabled: enabled && !!pinId, retry: false, diff --git a/frontend/src/apis/hooks/subtitles.ts b/frontend/src/apis/hooks/subtitles.ts index 50f22c9f7b..89910c7ee4 100644 --- a/frontend/src/apis/hooks/subtitles.ts +++ b/frontend/src/apis/hooks/subtitles.ts @@ -1,7 +1,7 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { QueryKeys } from "@/apis/queries/keys"; import api from "@/apis/raw"; -import { BatchTranslateItem } from "@/apis/raw/subtitles"; +import { BatchAction, BatchItem, BatchOptions } from "@/apis/raw/subtitles"; export function useSubtitleAction() { const client = useQueryClient(); @@ -147,6 +147,14 @@ export function useSubtitleInfos(names: string[]) { }); } +export function useSubtitleContents(subtitlePath: string) { + return useQuery({ + queryKey: [QueryKeys.Subtitles, subtitlePath], + queryFn: () => api.subtitles.contents(subtitlePath), + staleTime: Infinity, + }); +} + export function useRefTracksByEpisodeId( subtitlesPath: string, sonarrEpisodeId: number, @@ -183,24 +191,54 @@ export function useRefTracksByMovieId( }); } -export function useBatchTranslate() { +export function useUpgradableItems() { + return useQuery({ + queryKey: [QueryKeys.Subtitles, "upgradable"], + queryFn: () => api.subtitles.upgradable(), + refetchInterval: 60000, + }); +} + +export function useBatchAction() { const client = useQueryClient(); return useMutation({ - mutationKey: [QueryKeys.Subtitles, "batch-translate"], - mutationFn: (items: BatchTranslateItem[]) => - api.subtitles.batchTranslate(items), + mutationKey: [QueryKeys.Subtitles, "batch"], + mutationFn: (params: { + items: BatchItem[]; + action: BatchAction; + options?: BatchOptions; + }) => api.subtitles.batch(params.items, params.action, params.options), onSuccess: () => { - // Invalidate wanted queries to refresh the lists void client.invalidateQueries({ - queryKey: [QueryKeys.Series, QueryKeys.Wanted], + queryKey: [QueryKeys.Series], + }); + void client.invalidateQueries({ + queryKey: [QueryKeys.Movies], }); void client.invalidateQueries({ - queryKey: [QueryKeys.Movies, QueryKeys.Wanted], + queryKey: [QueryKeys.History], }); - // Also invalidate translator jobs to show new jobs void client.invalidateQueries({ queryKey: [QueryKeys.Translator], }); }, }); } + +export function useSubtitleContent( + mediaType: string | undefined, + mediaId: number | undefined, + language: string | undefined, +) { + return useQuery({ + queryKey: [QueryKeys.Subtitles, "content", mediaType, mediaId, language], + queryFn: () => { + if (!mediaType || mediaId === undefined || !language) { + throw new Error("Missing parameters"); + } + return api.subtitles.getContent(mediaType, mediaId, language); + }, + enabled: !!mediaType && mediaId !== undefined && !!language, + staleTime: 5 * 60 * 1000, // 5 min, not Infinity - subtitle files can change + }); +} diff --git a/frontend/src/apis/hooks/system.ts b/frontend/src/apis/hooks/system.ts index 3229c141ff..49fb92aad2 100644 --- a/frontend/src/apis/hooks/system.ts +++ b/frontend/src/apis/hooks/system.ts @@ -274,7 +274,16 @@ export function useSystem() { mutationFn: (param: { username: string; password: string }) => api.system.login(param.username, param.password), - onSuccess: () => { + onSuccess: (data) => { + if ( + data && + typeof data === "object" && + "upgrade_token" in data && + data.upgrade_token + ) { + // Store opaque token (not password) for upgrade prompt + sessionStorage.setItem("password_upgrade_token", data.upgrade_token); + } // TODO: Hard-coded value window.location.replace(Environment.baseUrl); }, diff --git a/frontend/src/apis/hooks/translator.ts b/frontend/src/apis/hooks/translator.ts index cf6879458c..e8415c2abf 100644 --- a/frontend/src/apis/hooks/translator.ts +++ b/frontend/src/apis/hooks/translator.ts @@ -4,7 +4,13 @@ import client from "@/apis/raw/client"; export interface TranslatorJob { jobId: string; - status: "queued" | "processing" | "completed" | "failed" | "cancelled"; + status: + | "queued" + | "processing" + | "completed" + | "partial" + | "failed" + | "cancelled"; progress: number; message?: string; createdAt: string; @@ -14,6 +20,21 @@ export interface TranslatorJob { sourceLanguage?: string; targetLanguage?: string; filename?: string; + title?: string; + mediaType?: string; + model?: string; + jobName?: string; + totalLines?: number; + completedLines?: number; + totalBatches?: number; + completedBatches?: number; + tokensUsed?: number; + totalCost?: number; + elapsedSeconds?: number; + result?: { + model_used?: string; + tokens_used?: number; + }; } export interface TranslatorStatus { @@ -32,6 +53,10 @@ export interface TranslatorStatus { failed: number; total: number; }; + bazarr_queue?: { + pending: number; + running: number; + }; } export interface TranslatorJobsResponse { @@ -152,3 +177,36 @@ export function useTranslatorModels(enabled = true) { throwOnError: false, }); } + +export interface TranslatorTestResponse { + encryption?: { + status: string; + message: string; + } | null; + apiKey?: { + status: string; + label: string; + limitRemaining: number; + usage: number; + isFreeTier: boolean; + } | null; + error?: string; +} + +export interface TranslatorTestParams { + serviceUrl?: string; + apiKey?: string; + encryptionKey?: string; +} + +export function useTestTranslator() { + return useMutation({ + mutationFn: async (params?: TranslatorTestParams) => { + const response = await client.axios.post( + "/translator/test", + params, + ); + return response.data; + }, + }); +} diff --git a/frontend/src/apis/raw/base.ts b/frontend/src/apis/raw/base.ts index 16f7bd5f32..07e660675b 100644 --- a/frontend/src/apis/raw/base.ts +++ b/frontend/src/apis/raw/base.ts @@ -49,7 +49,7 @@ class BaseApi { protected postRaw( path: string, - data?: any, + data?: unknown, params?: LooseObject, ): Promise> { return client.axios.post(this.prefix + path, data, { params }); diff --git a/frontend/src/apis/raw/client.ts b/frontend/src/apis/raw/client.ts index f3c2f53a37..ae1d48cca1 100644 --- a/frontend/src/apis/raw/client.ts +++ b/frontend/src/apis/raw/client.ts @@ -9,9 +9,12 @@ import { setAuthenticated } from "@/utilities/event"; function GetErrorMessage(data: unknown, defaultMsg = "Unknown error"): string { if (typeof data === "string") { return data; - } else { - return defaultMsg; + } else if (typeof data === "object" && data !== null) { + const obj = data as Record; + if (typeof obj.error === "string") return obj.error; + if (typeof obj.message === "string") return obj.message; } + return defaultMsg; } class BazarrClient { diff --git a/frontend/src/apis/raw/plex.ts b/frontend/src/apis/raw/plex.ts index 4318cf5972..41950cea25 100644 --- a/frontend/src/apis/raw/plex.ts +++ b/frontend/src/apis/raw/plex.ts @@ -11,10 +11,10 @@ class NewPlexApi extends BaseApi { return response.data; } - async checkPin(pinId: string) { - // TODO: Can this be replaced with params instead of passing a variable in the path? + async checkPin(pinId: string, state?: string) { const response = await this.get>( `/oauth/pin/${pinId}/check`, + state ? { state } : undefined, ); return response.data; diff --git a/frontend/src/apis/raw/subtitles.ts b/frontend/src/apis/raw/subtitles.ts index a126ecc8a7..1594c43ef1 100644 --- a/frontend/src/apis/raw/subtitles.ts +++ b/frontend/src/apis/raw/subtitles.ts @@ -1,18 +1,70 @@ import BaseApi from "./base"; +import client from "./client"; -export interface BatchTranslateItem { - type: "episode" | "movie"; +export interface SubtitleContentResponse { + content: string; + encoding: string; + format: string; + language: string; + size: number; + lastModified: number; + mediaTitle?: string; + mediaId?: number; + episodeTitle?: string; +} + +export type BatchAction = + | "sync" + | "translate" + | "OCR_fixes" + | "common" + | "remove_HI" + | "remove_tags" + | "fix_uppercase" + | "reverse_rtl" + | "scan-disk" + | "search-missing" + | "upgrade"; + +export interface BatchItem { + type: "episode" | "movie" | "series"; sonarrSeriesId?: number; sonarrEpisodeId?: number; radarrId?: number; - sourceLanguage: string; - targetLanguage: string; - subtitlePath?: string; - forced?: boolean; - hi?: boolean; } -export interface BatchTranslateResponse { +export interface BatchOptions { + maxOffsetSeconds?: number; + noFixFramerate?: boolean; + gss?: boolean; + forceResync?: boolean; + fromLang?: string; + toLang?: string; +} + +/* eslint-disable camelcase -- backend API contract */ +interface BatchPayload { + max_offset_seconds?: number; + no_fix_framerate?: boolean; + gss?: boolean; + force_resync?: boolean; + from_lang?: string; + to_lang?: string; +} + +function toBatchPayload(options: BatchOptions): BatchPayload { + return { + max_offset_seconds: options.maxOffsetSeconds, + no_fix_framerate: options.noFixFramerate, + gss: options.gss, + force_resync: options.forceResync, + from_lang: options.fromLang, + to_lang: options.toLang, + }; +} +/* eslint-enable camelcase */ + +export interface BatchResponse { queued: number; skipped: number; errors: string[]; @@ -56,12 +108,39 @@ class SubtitlesApi extends BaseApi { await this.patch("", form, { action }); } - async batchTranslate( - items: BatchTranslateItem[], - ): Promise { - const response = await this.postRaw( - "/translate/batch", - { items }, + async batch( + items: BatchItem[], + action: BatchAction, + options?: BatchOptions, + ): Promise { + const response = await this.postRaw("/batch", { + items, + action, + options: options ? toBatchPayload(options) : undefined, + }); + return response.data; + } + + async upgradable(): Promise<{ movies: number[]; series: number[] }> { + const response = await this.get<{ movies: number[]; series: number[] }>( + "/upgradable", + ); + return response; + } + + async getContent(mediaType: string, mediaId: number, language: string) { + const base = mediaType === "episode" ? "episodes" : "movies"; + const url = `/${base}/${mediaId}/subtitles/${encodeURIComponent(language)}/content`; + const response = await client.axios.get(url); + return response.data; + } + + async contents(subtitlePath: string) { + const response = await this.get>( + "/contents", + { + subtitlePath, + }, ); return response.data; } diff --git a/frontend/src/apis/raw/system.ts b/frontend/src/apis/raw/system.ts index a53eb2edeb..24ba7f49d3 100644 --- a/frontend/src/apis/raw/system.ts +++ b/frontend/src/apis/raw/system.ts @@ -10,7 +10,19 @@ class SystemApi extends BaseApi { } async login(username: string, password: string) { - await this.post("/account", { username, password }, { action: "login" }); + const response = await this.post<{ + upgrade_hash?: boolean; + upgrade_token?: string; + }>("/account", { username, password }, { action: "login" }); + return response.data; + } + + async upgradePasswordHash(upgradeToken: string) { + await this.post( + "/account", + { password: upgradeToken }, + { action: "upgrade_hash" }, + ); } async logout() { diff --git a/frontend/src/assets/_bazarr.scss b/frontend/src/assets/_bazarr.scss index 55587c78d7..a6e8ca052c 100644 --- a/frontend/src/assets/_bazarr.scss +++ b/frontend/src/assets/_bazarr.scss @@ -1,53 +1,17 @@ @use "mantine" as *; -$color-brand-0: #f8f0fc; -$color-brand-1: #f3d9fa; -$color-brand-2: #eebefa; -$color-brand-3: #e599f7; -$color-brand-4: #da77f2; -$color-brand-5: #cc5de8; -$color-brand-6: #be4bdb; -$color-brand-7: #ae3ec9; -$color-brand-8: #9c36b5; -$color-brand-9: #862e9c; - -// Based on Mantine Cyan -$color-highlight-0: #e3fafc; -$color-highlight-1: #c5f6fa; -$color-highlight-2: #99e9f2; -$color-highlight-3: #66d9e8; -$color-highlight-4: #3bc9db; -$color-highlight-5: #22b8cf; -$color-highlight-6: #15aabf; -$color-highlight-7: #1098ad; -$color-highlight-8: #0c8599; -$color-highlight-9: #0b7285; - -// Based on Mantine Yellow -$color-warning-0: #fff9db; -$color-warning-1: #fff3bf; -$color-warning-2: #ffec99; -$color-warning-3: #ffe066; -$color-warning-4: #ffd43b; -$color-warning-5: #fcc419; -$color-warning-6: #fab005; -$color-warning-7: #f59f00; -$color-warning-8: #f08c00; -$color-warning-9: #e67700; - -// Based on Mantine Gray -$color-disabled-0: #f8f9fa; -$color-disabled-1: #f1f3f5; -$color-disabled-2: #e9ecef; -$color-disabled-3: #dee2e6; -$color-disabled-4: #ced4da; -$color-disabled-5: #adb5bd; -$color-disabled-6: #868e96; -$color-disabled-7: #495057; -$color-disabled-8: #343a40; -$color-disabled-9: #212529; - -$header-height: 64px; +$color-brand-0: #fff8e1; +$color-brand-1: #ffecb3; +$color-brand-2: #ffe082; +$color-brand-3: #ffd54f; +$color-brand-4: #ffca28; +$color-brand-5: #e68a00; +$color-brand-6: #b36b00; +$color-brand-7: #995c00; +$color-brand-8: #7a4900; +$color-brand-9: #5c3700; + +$header-height: 76px; :global { .table-long-break { @@ -82,6 +46,338 @@ $header-height: 64px; :root { @include dark { - --mantine-color-body: var(--mantine-color-dark-8); + --mantine-color-body: var(--mantine-color-dark-9); + } +} + +// Grain texture overlay (z-index above content, pointer-events:none) +body::before { + content: ""; + position: fixed; + inset: 0; + z-index: 10000; + pointer-events: none; + opacity: 0.07; + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.7' numOctaves='3' stitchTiles='stitch'/%3E%3CfeColorMatrix type='saturate' values='0'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); + background-repeat: repeat; + background-size: 256px 256px; + + @include light { + opacity: 0.035; + mix-blend-mode: multiply; + } +} + +// Ambient atmospheric glow (z-index above content backgrounds, below grain) +body::after { + content: ""; + position: fixed; + inset: 0; + z-index: 9999; + pointer-events: none; + background: + radial-gradient( + ellipse at 15% 10%, + rgba(230, 138, 0, 0.12) 0%, + transparent 50% + ), + radial-gradient( + ellipse at 85% 90%, + rgba(26, 138, 138, 0.08) 0%, + transparent 50% + ), + radial-gradient( + ellipse at 50% 50%, + rgba(140, 80, 200, 0.05) 0%, + transparent 60% + ); + + @include light { + display: none; + } +} + +#root { + position: relative; + z-index: 1; +} + +/* Shared tokens (scheme-independent) */ +:root { + /* Border radius scale */ + --bz-radius-xs: 6px; + --bz-radius-sm: 8px; + --bz-radius-md: 10px; + --bz-radius-lg: 14px; + --bz-radius-xl: 20px; + + /* Animation tokens */ + --bz-ease-standard: cubic-bezier(0.2, 0, 0, 1); + --bz-ease-enter: cubic-bezier(0, 0, 0.2, 1); + --bz-ease-exit: cubic-bezier(0.4, 0, 1, 1); + --bz-duration-fast: 120ms; + --bz-duration-normal: 150ms; + --bz-duration-slow: 250ms; + --bz-duration-card: 300ms; + --bz-duration-page: 200ms; + + /* Stagger tokens */ + --bz-stagger-delay: 40ms; +} + +:root[data-mantine-color-scheme="dark"] { + --bz-surface-ground: #0c0b1a; + --bz-surface-base: #121125; + --bz-surface-raised: #1a1a2e; + --bz-surface-card: #22223a; + --bz-surface-card-hover: #2a2a45; + --bz-surface-overlay: #2a2a45; + --bz-stat-processing: #ffca28; + --bz-stat-queued: #4dabf7; + --bz-stat-completed: #69db7c; + --bz-stat-failed: #ff6b6b; + --bz-shadow-float: 0 4px 12px rgba(0, 0, 0, 0.5); + + /* Text hierarchy */ + --bz-text-primary: #e8e8f0; + --bz-text-secondary: #b8b8cc; + --bz-text-tertiary: #8e8fa3; + --bz-text-disabled: #6c6d85; + + /* Border opacity scale */ + --bz-border-divider: rgba(255, 255, 255, 0.04); + --bz-border-card: rgba(255, 255, 255, 0.05); + --bz-border-interactive: rgba(255, 255, 255, 0.06); + --bz-border-hover: rgba(255, 255, 255, 0.07); + + /* Hover tokens */ + --bz-hover-bg: rgba(255, 255, 255, 0.04); + --bz-hover-bg-emphasis: rgba(255, 255, 255, 0.07); +} + +:root[data-mantine-color-scheme="light"] { + --bz-text-primary: #12121f; + --bz-text-secondary: #343a40; + --bz-text-tertiary: #656d76; + --bz-text-disabled: #909aa5; + + --bz-border-divider: rgba(0, 0, 0, 0.06); + --bz-border-card: rgba(0, 0, 0, 0.08); + --bz-border-interactive: rgba(0, 0, 0, 0.1); + --bz-border-hover: rgba(0, 0, 0, 0.12); + + --bz-surface-ground: #f8f9fa; + --bz-surface-base: #ffffff; + --bz-surface-raised: #f1f3f5; + --bz-surface-card: #ffffff; + --bz-surface-card-hover: #f8f9fa; + --bz-surface-overlay: #ffffff; + --bz-stat-processing: #e67700; + --bz-stat-queued: #1971c2; + --bz-stat-completed: #2b8a3e; + --bz-stat-failed: #e03131; + --bz-shadow-float: 0 4px 12px rgba(0, 0, 0, 0.1); + + --bz-hover-bg: rgba(0, 0, 0, 0.04); + --bz-hover-bg-emphasis: rgba(0, 0, 0, 0.07); +} + +// Floaty main content area +:global { + .mantine-AppShell-main { + background: var(--bz-surface-base); + margin: 0 8px 8px 0; + border-radius: var(--bz-radius-xl); + border: 1px solid var(--bz-border-card); + } + + .mantine-AppShell-navbar { + background: transparent; + border-right: none; + } + + .mantine-AppShell-header { + background: transparent !important; + border: none !important; + padding: 8px !important; + } + + .mantine-AppShell-root { + background: var(--bz-surface-ground); + } +} + +// Global Paper/Card styling +:global { + .mantine-Paper-root { + border-radius: var(--bz-radius-lg); + + &[data-with-border] { + border-color: var(--bz-border-card); + } + } + + .mantine-Alert-root { + border-radius: var(--bz-radius-lg); + } + + .mantine-Card-root { + background: var(--bz-surface-card); + border-radius: var(--bz-radius-lg); + border: 1px solid var(--bz-border-card); + box-shadow: none; + transition: + background var(--bz-duration-normal) var(--bz-ease-standard), + border-color var(--bz-duration-normal) var(--bz-ease-standard); + + &:hover { + border-color: var(--bz-border-hover); + } + } + + // Tabs styling + .mantine-Tabs-tab { + transition: + background var(--bz-duration-normal) var(--bz-ease-standard), + color var(--bz-duration-normal) var(--bz-ease-standard), + border-color var(--bz-duration-normal) var(--bz-ease-standard); + } + + // Switch styling + .mantine-Switch-track { + transition: + background-color var(--bz-duration-normal) var(--bz-ease-standard), + border-color var(--bz-duration-normal) var(--bz-ease-standard); + } + + // Checkbox styling + .mantine-Checkbox-input { + border-radius: var(--bz-radius-xs); + transition: + background-color var(--bz-duration-normal) var(--bz-ease-standard), + border-color var(--bz-duration-normal) var(--bz-ease-standard); + } + + // Anchor/link styling + .mantine-Anchor-root { + transition: color var(--bz-duration-normal) var(--bz-ease-standard); + } + + // Tooltip styling + .mantine-Tooltip-tooltip { + background: var(--bz-surface-overlay); + border: 1px solid var(--bz-border-card); + border-radius: var(--bz-radius-sm); + color: var(--bz-text-primary); + box-shadow: var(--bz-shadow-float); + } + + // Popover styling + .mantine-Popover-dropdown { + background: var(--bz-surface-overlay); + border: 1px solid var(--bz-border-card); + border-radius: var(--bz-radius-lg); + box-shadow: var(--bz-shadow-float); + } +} + +// Global dropdown/menu/combobox styling +:global { + .mantine-Menu-dropdown, + .mantine-Combobox-dropdown, + .mantine-Select-dropdown, + .mantine-MultiSelect-dropdown, + .mantine-Autocomplete-dropdown { + background: var(--bz-surface-overlay); + border: 1px solid var(--bz-border-card); + border-radius: var(--bz-radius-lg); + box-shadow: var(--bz-shadow-float); + } + + .mantine-Menu-item { + border-radius: var(--bz-radius-sm); + color: var(--bz-text-primary); + transition: background var(--bz-duration-normal) var(--bz-ease-standard); + + &:hover, + &[data-hovered] { + background: var(--bz-hover-bg); + } + } + + .mantine-Combobox-option { + border-radius: var(--bz-radius-sm); + transition: background var(--bz-duration-normal) var(--bz-ease-standard); + + &:hover, + &[data-combobox-selected], + &[data-hovered] { + background: var(--bz-hover-bg); + } + } + + .mantine-Menu-label { + color: var(--bz-text-disabled); + } + + .mantine-Menu-divider { + border-color: var(--bz-border-divider); + } +} + +// Card reveal entry animation +@keyframes bz-card-reveal { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +// Apply card reveal to Mantine Cards and bordered Papers +:global { + .mantine-Card-root { + animation: bz-card-reveal var(--bz-duration-card) var(--bz-ease-enter) both; + } + + // Stagger cards inside Stack/SimpleGrid containers + .mantine-Stack-root, + .mantine-SimpleGrid-root { + > .mantine-Card-root, + > .mantine-Paper-root[data-with-border] { + animation: bz-card-reveal var(--bz-duration-card) var(--bz-ease-enter) + both; + + @for $i from 1 through 6 { + &:nth-child(#{$i}) { + animation-delay: calc(#{$i - 1} * var(--bz-stagger-delay)); + } + } + + // Items beyond 6th appear without extra stagger + &:nth-child(n + 7) { + animation-delay: calc(5 * var(--bz-stagger-delay)); + } + } + } +} + +// Reduced motion support +@media (prefers-reduced-motion: reduce) { + *, + *::before, + *::after { + animation-duration: 0.01ms !important; + animation-delay: 0ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + + body::after { + display: none !important; } } diff --git a/frontend/src/assets/_variables.module.scss b/frontend/src/assets/_variables.module.scss index c707980bfe..c3683f5ab6 100644 --- a/frontend/src/assets/_variables.module.scss +++ b/frontend/src/assets/_variables.module.scss @@ -1,5 +1,7 @@ $navbar-width: 200; +$surface-ground: #0c0b1a; + :export { colorBrand0: bazarr.$color-brand-0; colorBrand1: bazarr.$color-brand-1; @@ -15,4 +17,6 @@ $navbar-width: 200; headerHeight: bazarr.$header-height; navBarWidth: $navbar-width; + + surfaceGround: $surface-ground; } diff --git a/frontend/src/assets/action_icon.module.scss b/frontend/src/assets/action_icon.module.scss index 3ff702f35b..79d71034d1 100644 --- a/frontend/src/assets/action_icon.module.scss +++ b/frontend/src/assets/action_icon.module.scss @@ -1,16 +1,18 @@ @layer mantine { .root { --ai-bg: transparent; + border-radius: var(--bz-radius-sm); + transition: + background var(--bz-duration-normal) var(--bz-ease-standard), + color var(--bz-duration-normal) var(--bz-ease-standard); - @include mantine.light { - color: var(--mantine-color-dark-2); - --ai-hover: var(--mantine-color-gray-1); - --ai-hover-color: var(--mantine-color-gray-1); - } + color: var(--bz-text-secondary); + --ai-hover: var(--bz-hover-bg); + --ai-hover-color: var(--bz-text-primary); - @include mantine.dark { - color: var(--mantine-color-dark-0); - --ai-hover: var(--mantine-color-gray-8); + &[data-variant="gradient"], + &[data-variant="filled"] { + color: #fff; } } } diff --git a/frontend/src/assets/badge.module.scss b/frontend/src/assets/badge.module.scss index e0bd86a998..07c37e5a3e 100644 --- a/frontend/src/assets/badge.module.scss +++ b/frontend/src/assets/badge.module.scss @@ -1,51 +1,188 @@ -@use "sass:color"; - @layer mantine { .root { - background-color: color.adjust(bazarr.$color-brand-6, $alpha: -0.8); + border-radius: var(--bz-radius-sm); + font-size: 10px; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + padding: 2px 8px; + transition: + background var(--bz-duration-fast) var(--bz-ease-standard), + border-color var(--bz-duration-fast) var(--bz-ease-standard); - &[data-variant="warning"] { - color: color.adjust(bazarr.$color-warning-2, $lightness: 80%); - background-color: color.adjust(bazarr.$color-warning-6, $alpha: -0.8); + // Default variant (ghost): neutral for audio tracks and general use + background-color: rgba(255, 255, 255, 0.05); + color: #a9aabb; + border: 1px solid rgba(255, 255, 255, 0.07); + + &:hover { + background-color: rgba(255, 255, 255, 0.1); + border-color: rgba(255, 255, 255, 0.12); } + // Highlight variant (green): available subtitles &[data-variant="highlight"] { - color: color.adjust(bazarr.$color-highlight-2, $lightness: 100%); - background-color: color.adjust(bazarr.$color-highlight-5, $alpha: -0.8); + background: rgba(105, 219, 124, 0.12); + color: #69db7c; + border: 1px solid rgba(105, 219, 124, 0.18); + + &:hover { + background: rgba(105, 219, 124, 0.17); + border-color: rgba(105, 219, 124, 0.23); + } + } + + // Warning variant (amber): upgradeable subtitles + &[data-variant="warning"] { + background: rgba(230, 138, 0, 0.12); + color: #ffd54f; + border: 1px solid rgba(230, 138, 0, 0.18); + + &:hover { + background: rgba(230, 138, 0, 0.17); + border-color: rgba(230, 138, 0, 0.23); + } } + // Disabled variant (ghost dim): embedded/disabled subtitles &[data-variant="disabled"] { - color: color.adjust(bazarr.$color-disabled-0, $lightness: 100%); - background-color: color.adjust(bazarr.$color-disabled-7, $alpha: -0.8); + background: rgba(255, 255, 255, 0.03); + color: #6c6d85; + border: 1px solid rgba(255, 255, 255, 0.04); + + &:hover { + background: rgba(255, 255, 255, 0.06); + border-color: rgba(255, 255, 255, 0.08); + } + } + + // HI subs variant (warm): hearing impaired subtitles + &[data-variant="hi"] { + background: rgba(179, 107, 0, 0.1); + color: #ffd54f; + border: 1px solid rgba(179, 107, 0, 0.18); + + &:hover { + background: rgba(179, 107, 0, 0.15); + border-color: rgba(179, 107, 0, 0.23); + } + } + + // Missing variant: missing subtitles + &[data-variant="missing"] { + background: rgba(255, 107, 107, 0.06); + color: #ff8787; + border: 1px dashed rgba(255, 107, 107, 0.4); + + &:hover { + background: rgba(255, 107, 107, 0.11); + border-color: rgba(255, 107, 107, 0.5); + } } - &[data-variant="light"] { - color: var(--mantine-color-dark-0); - background-color: color.adjust(bazarr.$color-disabled-9, $alpha: -0.8); + // Preserve passthrough for Mantine built-in variants used elsewhere + &[data-variant="light"], + &[data-variant="outline"] { + color: unset; + background: unset; + background-color: unset; + border: unset; } + &[data-variant="gradient"] { + color: #fff; + border: unset; + } + + &[data-variant="transparent"] { + color: unset; + background: unset; + border: unset; + } + + // Light mode overrides @include mantine.light { - color: bazarr.$color-brand-6; - background-color: color.adjust(bazarr.$color-brand-3, $alpha: -0.8); + background-color: rgba(0, 0, 0, 0.05); + color: #495057; + border: 1px solid rgba(0, 0, 0, 0.08); + + &:hover { + background-color: rgba(0, 0, 0, 0.08); + border-color: rgba(0, 0, 0, 0.12); + } &[data-variant="warning"] { - color: color.adjust(bazarr.$color-warning-7, $lightness: -100%); - background-color: color.adjust(bazarr.$color-warning-6, $alpha: -0.8); + background: rgba(230, 138, 0, 0.1); + color: #995c00; + border: 1px solid rgba(230, 138, 0, 0.2); + + &:hover { + background: rgba(230, 138, 0, 0.15); + border-color: rgba(230, 138, 0, 0.28); + } } &[data-variant="disabled"] { - color: color.adjust(bazarr.$color-disabled-6, $lightness: -100%); - background-color: color.adjust(bazarr.$color-disabled-4, $alpha: -0.8); + background: rgba(0, 0, 0, 0.03); + color: #adb5bd; + border: 1px solid rgba(0, 0, 0, 0.06); + + &:hover { + background: rgba(0, 0, 0, 0.05); + border-color: rgba(0, 0, 0, 0.1); + } } &[data-variant="highlight"] { - color: color.adjust(bazarr.$color-highlight-6, $lightness: -100%); - background-color: color.adjust(bazarr.$color-highlight-5, $alpha: -0.8); + background: rgba(105, 219, 124, 0.1); + color: #2b8a3e; + border: 1px solid rgba(105, 219, 124, 0.2); + + &:hover { + background: rgba(105, 219, 124, 0.15); + border-color: rgba(105, 219, 124, 0.28); + } + } + + &[data-variant="missing"] { + background: rgba(255, 107, 107, 0.06); + color: #e03131; + border: 1px dashed rgba(255, 107, 107, 0.4); + + &:hover { + background: rgba(255, 107, 107, 0.1); + border-color: rgba(255, 107, 107, 0.5); + } + } + + &[data-variant="hi"] { + background: rgba(179, 107, 0, 0.1); + color: #7a4900; + border: 1px solid rgba(179, 107, 0, 0.2); + + &:hover { + background: rgba(179, 107, 0, 0.15); + border-color: rgba(179, 107, 0, 0.28); + } + } + + &[data-variant="light"], + &[data-variant="outline"] { + color: unset; + background: unset; + border: unset; + } + + &[data-variant="gradient"] { + color: #fff; + background-color: transparent; + border: none; } - &[data-variant="light"] { - color: var(--mantine-color-black); - background-color: var(--mantine-color-gray-5); + &[data-variant="transparent"] { + color: unset; + background: unset; + border: unset; } } } diff --git a/frontend/src/assets/button.module.scss b/frontend/src/assets/button.module.scss index 3ac8bca91f..14b9a3a879 100644 --- a/frontend/src/assets/button.module.scss +++ b/frontend/src/assets/button.module.scss @@ -1,7 +1,16 @@ @layer mantine { .root { - @include mantine.dark { - color: var(--mantine-color-dark-0); + border-radius: var(--bz-radius-sm); + transition: + background var(--bz-duration-normal) var(--bz-ease-standard), + transform var(--bz-duration-normal) var(--bz-ease-standard), + box-shadow var(--bz-duration-normal) var(--bz-ease-standard); + + color: var(--bz-text-primary); + + &[data-variant="filled"], + &[data-variant="gradient"] { + color: #fff; } &[data-variant="danger"] { @@ -11,8 +20,10 @@ } .root:disabled { - @include mantine.dark { - color: var(--mantine-color-dark-9); + color: var(--bz-text-disabled); + + @include mantine.light { + color: var(--bz-text-tertiary); } } } diff --git a/frontend/src/assets/input.module.scss b/frontend/src/assets/input.module.scss new file mode 100644 index 0000000000..7d29daebcf --- /dev/null +++ b/frontend/src/assets/input.module.scss @@ -0,0 +1,24 @@ +@layer mantine { + .input { + border-radius: var(--bz-radius-md); + background: var(--bz-hover-bg); + border: 1px solid var(--bz-border-interactive); + transition: + background var(--bz-duration-normal) var(--bz-ease-standard), + border-color var(--bz-duration-normal) var(--bz-ease-standard); + + &::placeholder { + color: var(--bz-text-tertiary); + } + + &:hover { + background: var(--bz-hover-bg-emphasis); + } + + &:focus, + &:focus-within { + background: var(--bz-hover-bg-emphasis); + border-color: var(--bz-border-hover); + } + } +} diff --git a/frontend/src/assets/pagination.module.scss b/frontend/src/assets/pagination.module.scss index 2b66d75103..f5c9e924bb 100644 --- a/frontend/src/assets/pagination.module.scss +++ b/frontend/src/assets/pagination.module.scss @@ -1,3 +1,15 @@ -.control { - --pagination-active-bg: var(--mantine-color-brand-filled); +@layer mantine { + .control { + --pagination-active-bg: var(--mantine-color-brand-filled); + border-radius: var(--bz-radius-xs); + transition: + background var(--bz-duration-normal) var(--bz-ease-standard), + border-color var(--bz-duration-normal) var(--bz-ease-standard); + + color: var(--bz-text-secondary); + + &:hover:not([data-active]) { + background: var(--bz-hover-bg); + } + } } diff --git a/frontend/src/assets/progress.module.scss b/frontend/src/assets/progress.module.scss new file mode 100644 index 0000000000..3b99a26dd0 --- /dev/null +++ b/frontend/src/assets/progress.module.scss @@ -0,0 +1,9 @@ +$noise-svg: url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.65' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.15'/%3E%3C/svg%3E"); + +@layer mantine { + .section { + background-image: $noise-svg !important; + background-size: 128px 128px !important; + background-repeat: repeat !important; + } +} diff --git a/frontend/src/components/StateIcon.tsx b/frontend/src/components/StateIcon.tsx index 23bb2d507d..4e164729fd 100644 --- a/frontend/src/components/StateIcon.tsx +++ b/frontend/src/components/StateIcon.tsx @@ -73,7 +73,7 @@ const StateIcon: FunctionComponent = ({ - + Scoring Criteria diff --git a/frontend/src/components/SubtitleToolsMenu.tsx b/frontend/src/components/SubtitleToolsMenu.tsx index 5f0fbc3c30..c92e073dbf 100644 --- a/frontend/src/components/SubtitleToolsMenu.tsx +++ b/frontend/src/components/SubtitleToolsMenu.tsx @@ -1,10 +1,12 @@ import { FunctionComponent, ReactElement, useCallback, useMemo } from "react"; import { Divider, List, Menu, MenuProps, ScrollArea } from "@mantine/core"; import { + faAlignJustify, faClock, faCode, faDeaf, faExchangeAlt, + faEye, faFaceGrinStars, faFilm, faImage, @@ -26,7 +28,9 @@ import { TranslationModal } from "@/components/forms/TranslationForm"; import { useModals } from "@/modules/modals"; import { ModalComponent } from "@/modules/modals/WithModal"; import { task } from "@/modules/task"; +import { toPython } from "@/utilities"; import { SyncSubtitleModal } from "./forms/SyncSubtitleForm"; +import { TwoPointFitModal } from "./forms/TwoPointFit"; export interface ToolOptions { key: string; @@ -99,6 +103,12 @@ export function useTools() { name: "Adjust Times...", modal: TimeOffsetModal, }, + { + key: "two_point_fit", + icon: faAlignJustify, + name: "Two-Point Fit...", + modal: TwoPointFitModal, + }, { key: "translation", icon: faLanguage, @@ -114,7 +124,12 @@ interface Props { selections: FormType.ModifySubtitle[]; children?: ReactElement; menu?: Omit; - onAction?: (action: "delete" | "search") => void; + onAction?: (action: "delete" | "search" | "view") => void; + // For missing subtitle translation + missingLanguage?: Subtitle; + translationSources?: Subtitle[]; + mediaId?: number; + mediaType?: "episode" | "movie"; } const SubtitleToolsMenu: FunctionComponent = ({ @@ -122,6 +137,10 @@ const SubtitleToolsMenu: FunctionComponent = ({ children, menu, onAction, + missingLanguage, + translationSources, + mediaId, + mediaType, }) => { const { mutateAsync } = useSubtitleAction(); @@ -146,30 +165,83 @@ const SubtitleToolsMenu: FunctionComponent = ({ const modals = useModals(); const disabledTools = selections.length === 0; + const isMissing = !!missingLanguage; + const hasSources = (translationSources ?? []).length > 0; return ( {children} Tools - {tools.map((tool) => ( + {tools.map((tool) => { + // "Translate" for missing subs: show as submenu with source options + if (tool.key === "translation" && isMissing) { + return null; // handled below in Actions + } + + return ( + } + onClick={() => { + if (tool.modal) { + modals.openContextModal(tool.modal, { selections }); + } else { + process(tool.key, tool.name); + } + }} + > + {tool.name} + + ); + })} + + Actions + {/* Translate from source โ€” for missing subtitles */} + {isMissing && hasSources && ( + <> + {translationSources!.map((source) => ( + } + onClick={async () => { + await mutateAsync({ + action: "translate", + form: { + id: mediaId!, + type: mediaType!, + language: missingLanguage.code2, + path: source.path!, + forced: toPython(missingLanguage.forced), + hi: toPython(missingLanguage.hi), + }, + }); + }} + > + Translate from {source.name || source.code2} + {source.hi ? " (HI)" : ""} + + ))} + + )} + {isMissing && !hasSources && ( } - onClick={() => { - if (tool.modal) { - modals.openContextModal(tool.modal, { selections }); - } else { - process(tool.key, tool.name); - } - }} + disabled + leftSection={} > - {tool.name} + No source subtitles to translate from - ))} - - Actions + )} + } + onClick={() => { + onAction?.("view"); + }} + > + View + } diff --git a/frontend/src/components/TranslatorStatus.module.css b/frontend/src/components/TranslatorStatus.module.css new file mode 100644 index 0000000000..70d62bcbd3 --- /dev/null +++ b/frontend/src/components/TranslatorStatus.module.css @@ -0,0 +1,63 @@ +@keyframes heartbeat { + 0%, + 100% { + opacity: 1; + transform: scale(1); + } + 14% { + opacity: 1; + transform: scale(1.15); + } + 28% { + opacity: 1; + transform: scale(1); + } + 42% { + opacity: 1; + transform: scale(1.1); + } + 56% { + opacity: 1; + transform: scale(1); + } +} + +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.promptConnected { + animation: heartbeat 1.5s ease-in-out infinite; + display: inline-block; +} + +.statCard { + transition: var(--bz-transition-lift); + cursor: default; + border-left-width: 3px; + border-left-style: solid; +} + +.statCard:hover { + transform: translateY(-2px); + background-color: var(--bz-surface-card-hover); +} + +.spin { + animation: spin 1s linear infinite; +} + +@media (prefers-reduced-motion: reduce) { + .promptConnected, + .spin { + animation: none; + } + .statCard:hover { + transform: none; + } +} diff --git a/frontend/src/components/TranslatorStatus.tsx b/frontend/src/components/TranslatorStatus.tsx index bff899b218..44552d3c71 100644 --- a/frontend/src/components/TranslatorStatus.tsx +++ b/frontend/src/components/TranslatorStatus.tsx @@ -1,9 +1,7 @@ -import { FunctionComponent, useState, useCallback } from "react"; +import { FunctionComponent, useCallback, useState } from "react"; import { - ActionIcon, Alert, Badge, - Box, Button, Card, Group, @@ -17,22 +15,19 @@ import { } from "@mantine/core"; import { faCheck, - faCircle, faClock, faExclamationTriangle, faRefresh, faSpinner, faTimes, - faTrash, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { TranslatorJob, useTranslatorJobs, useTranslatorStatus, - useCancelTranslatorJob, } from "@/apis/hooks/translator"; -import { useSettingValue } from "@/pages/Settings/utilities/hooks"; +import classes from "./TranslatorStatus.module.css"; interface StatusBadgeProps { status: TranslatorJob["status"]; @@ -43,6 +38,7 @@ const StatusBadge: FunctionComponent = ({ status }) => { queued: { color: "gray", icon: faClock, label: "Queued" }, processing: { color: "blue", icon: faSpinner, label: "Processing" }, completed: { color: "green", icon: faCheck, label: "Completed" }, + partial: { color: "yellow", icon: faExclamationTriangle, label: "Partial" }, failed: { color: "red", icon: faExclamationTriangle, label: "Failed" }, cancelled: { color: "orange", icon: faTimes, label: "Cancelled" }, }[status]; @@ -55,6 +51,7 @@ const StatusBadge: FunctionComponent = ({ status }) => { icon={config.icon} spin={status === "processing"} size="xs" + aria-hidden="true" /> } > @@ -65,55 +62,150 @@ const StatusBadge: FunctionComponent = ({ status }) => { interface JobRowProps { job: TranslatorJob; - onCancel: (id: string) => void; - isDeleting: boolean; } -const JobRow: FunctionComponent = ({ - job, - onCancel, - isDeleting, -}) => { - const canCancel = job.status === "queued"; +function formatDuration(seconds: number): string { + if (seconds < 60) return `${seconds.toFixed(0)}s`; + const min = Math.floor(seconds / 60); + const sec = Math.round(seconds % 60); + return `${min}m ${sec}s`; +} + +function formatCostValue(cost: number): string { + if (cost === 0) return "Free"; + if (cost < 0.01) return `$${cost.toFixed(4)}`; + return `$${cost.toFixed(2)}`; +} + +const JobRow: FunctionComponent = ({ job }) => { + const modelUsed = job.result?.model_used || job.model; + // Prefer top-level tokensUsed (live from API), fall back to result + const tokensUsed = job.tokensUsed || job.result?.tokens_used; + + // Build media title + const langPair = + job.sourceLanguage && job.targetLanguage + ? ` (${job.sourceLanguage.toUpperCase()} โ†’ ${job.targetLanguage.toUpperCase()})` + : ""; + const mediaTitle = job.title + ? `${job.title}${langPair}` + : job.filename + ? `${job.filename}${langPair}` + : langPair || "-"; + + // Duration โ€” prefer server-side elapsedSeconds + let durationStr = "-"; + let durationSec = 0; + if (job.elapsedSeconds) { + durationSec = job.elapsedSeconds; + durationStr = formatDuration(durationSec); + } else if (job.startedAt && job.completedAt) { + durationSec = + (new Date(job.completedAt).getTime() - + new Date(job.startedAt).getTime()) / + 1000; + durationStr = formatDuration(durationSec); + } else if (job.status === "processing" && job.startedAt) { + // eslint-disable-next-line react-hooks/purity + const elapsed = (Date.now() - new Date(job.startedAt).getTime()) / 1000; + durationStr = `${formatDuration(elapsed)}...`; + } + + // TPS + const tpsStr = + tokensUsed && durationSec > 0 + ? `${(tokensUsed / durationSec).toFixed(0)} t/s` + : "-"; + + // Progress โ€” show lines if available + const progressLabel = + job.completedLines && job.totalLines + ? `${job.completedLines}/${job.totalLines} lines` + : `${job.progress}%`; + + // Batch info for tooltip + const batchInfo = + job.completedBatches && job.totalBatches + ? `Batch ${job.completedBatches}/${job.totalBatches}` + : undefined; return ( - - - {job.jobId.substring(0, 8)}... + + + {mediaTitle} - + {job.error ? ( + + + + + + ) : ( + + )} {job.status === "processing" ? ( - + + + ) : ( - {job.progress}% + {progressLabel} )} - - {job.message || job.filename || "-"} + + {modelUsed || "-"} + + + + + {tokensUsed ? tokensUsed.toLocaleString() : "-"} + + + + + {job.totalCost != null && job.totalCost > 0 ? ( + + + {formatCostValue(job.totalCost)} + + + ) : ( + "-" + )} - {new Date(job.createdAt).toLocaleTimeString()} + + {durationStr} + - {canCancel && ( - onCancel(job.jobId)} - loading={isDeleting} - > - - - )} + + {tpsStr} + ); @@ -130,27 +222,35 @@ const StatCard: FunctionComponent = ({ value, color, }) => ( - - - {label} - - + + {value} + + {label} + ); interface TranslatorStatusPanelProps { enabled?: boolean; - savedApiKey?: string; - savedModel?: string; - savedMaxConcurrent?: number; } export const TranslatorStatusPanel: FunctionComponent< TranslatorStatusPanelProps -> = ({ enabled = true, savedApiKey, savedModel, savedMaxConcurrent }) => { - const [retryKey, setRetryKey] = useState(0); +> = ({ enabled = true }) => { + const [, setRetryKey] = useState(0); const { data: status, @@ -164,8 +264,6 @@ export const TranslatorStatusPanel: FunctionComponent< isError: jobsError, refetch: refetchJobs, } = useTranslatorJobs(enabled && !statusError); - const cancelJob = useCancelTranslatorJob(); - const handleRetry = useCallback(() => { setRetryKey((k) => k + 1); void refetchStatus(); @@ -177,8 +275,10 @@ export const TranslatorStatusPanel: FunctionComponent< return ( - - Connecting to AI Subtitle Translator... + ); @@ -195,7 +295,9 @@ export const TranslatorStatusPanel: FunctionComponent< color="yellow" title="AI Subtitle Translator Unavailable" mt="md" - icon={} + icon={ +